Merge pull request #1101 from odoo-dev/master-less-support-in-bundles-fme
[ADD] Support for LessCSS in asset bundles Also added option in order to include asset bundles in templates list available to the HTML editor
This commit is contained in:
@@ -202,10 +202,11 @@ class Website(openerp.addons.web.controllers.main.Home):
|
||||
return request.redirect(redirect)
|
||||
|
||||
@http.route('/website/customize_template_get', type='json', auth='user', website=True)
|
||||
def customize_template_get(self, xml_id, full=False):
|
||||
def customize_template_get(self, xml_id, full=False, bundles=False):
|
||||
""" Lists the templates customizing ``xml_id``. By default, only
|
||||
returns optional templates (which can be toggled on and off), if
|
||||
``full=True`` returns all templates customizing ``xml_id``
|
||||
``bundles=True`` returns also the asset bundles
|
||||
"""
|
||||
imd = request.registry['ir.model.data']
|
||||
view_model, view_theme_id = imd.get_object_reference(
|
||||
@@ -216,7 +217,7 @@ class Website(openerp.addons.web.controllers.main.Home):
|
||||
user_groups = set(user.groups_id)
|
||||
|
||||
views = request.registry["ir.ui.view"]\
|
||||
._views_get(request.cr, request.uid, xml_id, context=request.context)
|
||||
._views_get(request.cr, request.uid, xml_id, bundles=bundles, context=request.context)
|
||||
done = set()
|
||||
result = []
|
||||
for v in views:
|
||||
|
||||
@@ -37,7 +37,7 @@ class view(osv.osv):
|
||||
|
||||
# Returns all views (called and inherited) related to a view
|
||||
# Used by translation mechanism, SEO and optional templates
|
||||
def _views_get(self, cr, uid, view_id, options=True, context=None, root=True):
|
||||
def _views_get(self, cr, uid, view_id, options=True, bundles=False, context=None, root=True):
|
||||
""" For a given view ``view_id``, should return:
|
||||
|
||||
* the view itself
|
||||
@@ -57,13 +57,16 @@ class view(osv.osv):
|
||||
result = [view]
|
||||
|
||||
node = etree.fromstring(view.arch)
|
||||
for child in node.xpath("//t[@t-call]"):
|
||||
xpath = "//t[@t-call]"
|
||||
if bundles:
|
||||
xpath += "| //t[@t-call-assets]"
|
||||
for child in node.xpath(xpath):
|
||||
try:
|
||||
called_view = self._view_obj(cr, uid, child.get('t-call'), context=context)
|
||||
called_view = self._view_obj(cr, uid, child.get('t-call', child.get('t-call-assets')), context=context)
|
||||
except ValueError:
|
||||
continue
|
||||
if called_view not in result:
|
||||
result += self._views_get(cr, uid, called_view, options=options, context=context)
|
||||
result += self._views_get(cr, uid, called_view, options=options, bundles=bundles, context=context)
|
||||
|
||||
extensions = view.inherit_children_ids
|
||||
if not options:
|
||||
|
||||
@@ -23,8 +23,8 @@
|
||||
.navbar.navbar-inverse .cke_top {
|
||||
background: transparent;
|
||||
border: none;
|
||||
-webkit-box-shadow: none;
|
||||
-moz-box-shadow: none;
|
||||
-webkit-box-shadow: none;
|
||||
box-shadow: none;
|
||||
-ms-filter: "alpha(opacity=50)";
|
||||
}
|
||||
@@ -57,13 +57,11 @@
|
||||
padding: 2px;
|
||||
margin: 0;
|
||||
z-index: 20000;
|
||||
background: #414141, -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #646060), color-stop(100%, #262626));
|
||||
background: #414141, -webkit-linear-gradient(#646060, #262626);
|
||||
background: #414141, -moz-linear-gradient(#646060, #262626);
|
||||
background: #414141, -o-linear-gradient(#646060, #262626);
|
||||
background: #414141, -webkit-linear-gradient(#646060, #262626);
|
||||
background: #414141, linear-gradient(#646060, #262626);
|
||||
-webkit-box-sizing: border-box;
|
||||
-moz-box-sizing: border-box;
|
||||
-webkit-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.oe_website_editorbar li {
|
||||
@@ -225,8 +223,9 @@ ul.oe_menu_editor .disclose {
|
||||
|
||||
/* ---- RTE ---- {{{ */
|
||||
.oe_editable .btn, .btn.oe_editable {
|
||||
-webkit-user-select: auto;
|
||||
-moz-user-select: auto;
|
||||
-ms-user-select: auto;
|
||||
-webkit-user-select: auto;
|
||||
user-select: auto;
|
||||
cursor: text !important;
|
||||
}
|
||||
@@ -384,10 +383,8 @@ ul.oe_menu_editor .disclose {
|
||||
height: 660px;
|
||||
background-color: black;
|
||||
border: 2px solid #1c1f1f;
|
||||
-webkit-border-radius: 10px;
|
||||
-moz-border-radius: 10px;
|
||||
-ms-border-radius: 10px;
|
||||
-o-border-radius: 10px;
|
||||
-webkit-border-radius: 10px;
|
||||
border-radius: 10px;
|
||||
margin: auto;
|
||||
top: 0;
|
||||
@@ -410,13 +407,13 @@ ul.oe_menu_editor .disclose {
|
||||
color: #1c1f1f;
|
||||
}
|
||||
.oe_mobile_preview.modal .modal-content .modal-header .close {
|
||||
color: lightgrey;
|
||||
filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=100);
|
||||
color: lightgray;
|
||||
filter: progid:DXImageTransform.Microsoft.Alpha(enabled=false);
|
||||
opacity: 1;
|
||||
}
|
||||
.oe_mobile_preview.modal .modal-content .modal-header .close:hover {
|
||||
color: #e00101;
|
||||
filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=100);
|
||||
filter: progid:DXImageTransform.Microsoft.Alpha(enabled=false);
|
||||
opacity: 1;
|
||||
}
|
||||
.oe_mobile_preview.modal .modal-content .modal-body {
|
||||
@@ -447,10 +444,8 @@ ul.oe_menu_editor .disclose {
|
||||
}
|
||||
.oe_seo_configuration .oe_seo_keyword {
|
||||
padding: 0.2em 0.4em 0.2em 0.5em;
|
||||
-webkit-border-radius: 0.4em;
|
||||
-moz-border-radius: 0.4em;
|
||||
-ms-border-radius: 0.4em;
|
||||
-o-border-radius: 0.4em;
|
||||
-webkit-border-radius: 0.4em;
|
||||
border-radius: 0.4em;
|
||||
}
|
||||
.oe_seo_configuration li.oe_seo_preview_g {
|
||||
@@ -554,3 +549,8 @@ ul.oe_menu_editor .disclose {
|
||||
filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=0);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.oe_include_bundles {
|
||||
font-weight: normal;
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
@@ -477,6 +477,10 @@ $infobar_height: 20px
|
||||
z-index: -1000
|
||||
+opacity(0)
|
||||
|
||||
.oe_include_bundles
|
||||
font-weight: normal
|
||||
padding: 0 8px
|
||||
|
||||
// }}}
|
||||
|
||||
// vim:tabstop=4:shiftwidth=4:softtabstop=4:fdm=marker:
|
||||
|
||||
@@ -70,6 +70,7 @@
|
||||
template: 'website.ace_view_editor',
|
||||
events: {
|
||||
'change #ace-view-list': 'displaySelectedView',
|
||||
'click .js_include_bundles': 'loadTemplates',
|
||||
'click button[data-action=save]': 'saveViews',
|
||||
'click button[data-action=format]': 'formatXml',
|
||||
'click button[data-action=close]': 'close',
|
||||
@@ -83,26 +84,7 @@
|
||||
var self = this;
|
||||
self.aceEditor = ace.edit(self.$('#ace-view-editor')[0]);
|
||||
self.aceEditor.setTheme("ace/theme/monokai");
|
||||
var viewId = $(document.documentElement).data('view-xmlid');
|
||||
openerp.jsonRpc('/website/customize_template_get', 'call', {
|
||||
'xml_id': viewId,
|
||||
'full': true,
|
||||
}).then(function (views) {
|
||||
self.loadViews.call(self, views);
|
||||
self.open.call(self);
|
||||
var curentHash = window.location.hash;
|
||||
var indexOfView = curentHash.indexOf("?view=");
|
||||
if (indexOfView >= 0) {
|
||||
var viewId = parseInt(curentHash.substring(indexOfView + 6, curentHash.length), 10);
|
||||
self.$('#ace-view-list').val(viewId).change();
|
||||
} else {
|
||||
if (views.length >= 2) {
|
||||
var mainTemplate = views[1];
|
||||
self.$('#ace-view-list').val(mainTemplate.id).trigger('change');
|
||||
}
|
||||
window.location.hash = hash;
|
||||
}
|
||||
});
|
||||
self.loadTemplates();
|
||||
|
||||
var $editor = self.$('.ace_editor');
|
||||
function resizeEditor (target) {
|
||||
@@ -150,8 +132,34 @@
|
||||
resizeEditor(readEditorWidth());
|
||||
resizeEditorHeight(this.getParent().$el.outerHeight()+2);
|
||||
},
|
||||
loadTemplates: function () {
|
||||
var self = this;
|
||||
var args = {
|
||||
xml_id: $(document.documentElement).data('view-xmlid'),
|
||||
full: true,
|
||||
bundles: this.$('.js_include_bundles')[0].checked
|
||||
};
|
||||
return openerp
|
||||
.jsonRpc('/website/customize_template_get', 'call', args)
|
||||
.then(function (views) {
|
||||
self.loadViews.call(self, views);
|
||||
self.open.call(self);
|
||||
var curentHash = window.location.hash;
|
||||
var indexOfView = curentHash.indexOf("?view=");
|
||||
if (indexOfView >= 0) {
|
||||
var viewId = parseInt(curentHash.substring(indexOfView + 6, curentHash.length), 10);
|
||||
self.$('#ace-view-list').val(viewId).change();
|
||||
} else {
|
||||
if (views.length >= 2) {
|
||||
var mainTemplate = views[1];
|
||||
self.$('#ace-view-list').val(mainTemplate.id).trigger('change');
|
||||
}
|
||||
window.location.hash = hash;
|
||||
}
|
||||
});
|
||||
},
|
||||
loadViews: function (views) {
|
||||
var $viewList = this.$('#ace-view-list');
|
||||
var $viewList = this.$('#ace-view-list').empty();
|
||||
_(this.buildViewGraph(views)).each(function (view) {
|
||||
if (!view.id) { return; }
|
||||
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
<select id="ace-view-list" class="form-control oe_view_list">
|
||||
<!-- filled in JS -->
|
||||
</select>
|
||||
<label class="oe_include_bundles">
|
||||
<input type="checkbox" class="js_include_bundles"/> Include Asset Bundles
|
||||
</label>
|
||||
<button data-action="save" type="submit" class="btn btn-success">Save</button>
|
||||
<button data-action="format" type="button" class="btn btn-warning">Format</button>
|
||||
<button data-action="close" type="button" class="btn btn-info">Close</button>
|
||||
|
||||
@@ -1001,18 +1001,9 @@ class AssetNotFound(AssetError):
|
||||
pass
|
||||
|
||||
class AssetsBundle(object):
|
||||
# Sass installation:
|
||||
#
|
||||
# sudo gem install sass compass bootstrap-sass
|
||||
#
|
||||
# If the following error is encountered:
|
||||
# 'ERROR: Cannot load compass.'
|
||||
# Use this:
|
||||
# sudo gem install compass --pre
|
||||
cmd_sass = ['sass', '--stdin', '-t', 'compressed', '--unix-newlines', '--compass', '-r', 'bootstrap-sass']
|
||||
cache = openerp.tools.lru.LRU(32)
|
||||
rx_css_import = re.compile("(@import[^;{]+;?)", re.M)
|
||||
rx_sass_import = re.compile("""(@import\s?['"]([^'"]+)['"])""")
|
||||
rx_preprocess_imports = re.compile("""(@import\s?['"]([^'"]+)['"](;?))""")
|
||||
rx_css_split = re.compile("\/\*\! ([a-f0-9-]+) \*\/")
|
||||
|
||||
def __init__(self, xmlid, debug=False, cr=None, uid=None, context=None, registry=None):
|
||||
@@ -1044,12 +1035,16 @@ class AssetsBundle(object):
|
||||
media = el.get('media')
|
||||
if el.tag == 'style':
|
||||
if atype == 'text/sass' or src.endswith('.sass'):
|
||||
self.stylesheets.append(SassAsset(self, inline=el.text, media=media))
|
||||
self.stylesheets.append(SassStylesheetAsset(self, inline=el.text, media=media))
|
||||
elif atype == 'text/less' or src.endswith('.less'):
|
||||
self.stylesheets.append(LessStylesheetAsset(self, inline=el.text, media=media))
|
||||
else:
|
||||
self.stylesheets.append(StylesheetAsset(self, inline=el.text, media=media))
|
||||
elif el.tag == 'link' and el.get('rel') == 'stylesheet' and self.can_aggregate(href):
|
||||
if href.endswith('.sass') or atype == 'text/sass':
|
||||
self.stylesheets.append(SassAsset(self, url=href, media=media))
|
||||
self.stylesheets.append(SassStylesheetAsset(self, url=href, media=media))
|
||||
elif href.endswith('.less') or atype == 'text/less':
|
||||
self.stylesheets.append(LessStylesheetAsset(self, url=href, media=media))
|
||||
else:
|
||||
self.stylesheets.append(StylesheetAsset(self, url=href, media=media))
|
||||
elif el.tag == 'script' and not src:
|
||||
@@ -1074,7 +1069,7 @@ class AssetsBundle(object):
|
||||
response = []
|
||||
if debug:
|
||||
if css and self.stylesheets:
|
||||
self.compile_sass()
|
||||
self.preprocess_css()
|
||||
for style in self.stylesheets:
|
||||
response.append(style.to_html())
|
||||
if js:
|
||||
@@ -1125,7 +1120,7 @@ class AssetsBundle(object):
|
||||
# Invalidate cache on version mismach
|
||||
self.cache.pop(key)
|
||||
if key not in self.cache:
|
||||
self.compile_sass()
|
||||
self.preprocess_css()
|
||||
content = '\n'.join(asset.minify() for asset in self.stylesheets)
|
||||
|
||||
if self.css_errors:
|
||||
@@ -1160,39 +1155,41 @@ class AssetsBundle(object):
|
||||
}
|
||||
""" % message.replace('"', '\\"')
|
||||
|
||||
def compile_sass(self):
|
||||
def preprocess_css(self):
|
||||
"""
|
||||
Checks if the bundle contains any sass content, then compiles it to css.
|
||||
Css compilation is done at the bundle level and not in the assets
|
||||
because they are potentially interdependant.
|
||||
Checks if the bundle contains any sass/less content, then compiles it to css.
|
||||
"""
|
||||
sass = [asset for asset in self.stylesheets if isinstance(asset, SassAsset)]
|
||||
if not sass:
|
||||
to_preprocess = [asset for asset in self.stylesheets if isinstance(asset, PreprocessedCSS)]
|
||||
if not to_preprocess:
|
||||
return
|
||||
source = '\n'.join([asset.get_source() for asset in sass])
|
||||
to_compile = [asset for asset in to_preprocess if type(asset) == type(to_preprocess[0])]
|
||||
if len(to_preprocess) != len(to_compile):
|
||||
self.css_errors.append("You can't mix css preprocessors languages in the same bundle. (%s)" % self.xmlid)
|
||||
command = to_compile[0].get_command()
|
||||
source = '\n'.join([asset.get_source() for asset in to_compile])
|
||||
|
||||
# move up all @import rules to the top and exclude file imports
|
||||
imports = []
|
||||
def push(matchobj):
|
||||
ref = matchobj.group(2)
|
||||
line = '@import "%s"' % ref
|
||||
line = '@import "%s"%s' % (ref, matchobj.group(3))
|
||||
if '.' not in ref and line not in imports and not ref.startswith(('.', '/', '~')):
|
||||
imports.append(line)
|
||||
return ''
|
||||
source = re.sub(self.rx_sass_import, push, source)
|
||||
source = re.sub(self.rx_preprocess_imports, push, source)
|
||||
imports.append(source)
|
||||
source = u'\n'.join(imports)
|
||||
|
||||
try:
|
||||
compiler = Popen(self.cmd_sass, stdin=PIPE, stdout=PIPE, stderr=PIPE)
|
||||
compiler = Popen(command, stdin=PIPE, stdout=PIPE, stderr=PIPE)
|
||||
except Exception:
|
||||
msg = "Could not find 'sass' program needed to compile sass/scss files"
|
||||
msg = "Could not execute command %r" % command[0]
|
||||
_logger.error(msg)
|
||||
self.css_errors.append(msg)
|
||||
return
|
||||
result = compiler.communicate(input=source.encode('utf-8'))
|
||||
if compiler.returncode:
|
||||
error = self.get_sass_error(''.join(result), source=source)
|
||||
error = self.get_preprocessor_error(''.join(result), source=source)
|
||||
_logger.warning(error)
|
||||
self.css_errors.append(error)
|
||||
return
|
||||
@@ -1200,15 +1197,18 @@ class AssetsBundle(object):
|
||||
fragments = self.rx_css_split.split(compiled)[1:]
|
||||
while fragments:
|
||||
asset_id = fragments.pop(0)
|
||||
asset = next(asset for asset in sass if asset.id == asset_id)
|
||||
asset = next(asset for asset in to_compile if asset.id == asset_id)
|
||||
asset._content = fragments.pop(0)
|
||||
|
||||
def get_sass_error(self, stderr, source=None):
|
||||
def get_preprocessor_error(self, stderr, source=None):
|
||||
# TODO: try to find out which asset the error belongs to
|
||||
error = stderr.split('Load paths')[0].replace(' Use --trace for backtrace.', '')
|
||||
if 'Cannot load compass' in error:
|
||||
error += "Maybe you should install the compass gem using this extra argument:\n\n" \
|
||||
" $ sudo gem install compass --pre\n"
|
||||
error += "This error occured while compiling the bundle '%s' containing:" % self.xmlid
|
||||
for asset in self.stylesheets:
|
||||
if isinstance(asset, SassAsset):
|
||||
if isinstance(asset, PreprocessedCSS):
|
||||
error += '\n - %s' % (asset.url if asset.url else '<inline sass>')
|
||||
return error
|
||||
|
||||
@@ -1373,11 +1373,8 @@ class StylesheetAsset(WebAsset):
|
||||
else:
|
||||
return '<style type="text/css"%s>%s</style>' % (media, self.with_header())
|
||||
|
||||
class SassAsset(StylesheetAsset):
|
||||
class PreprocessedCSS(StylesheetAsset):
|
||||
html_url = '%s.css'
|
||||
rx_indent = re.compile(r'^( +|\t+)', re.M)
|
||||
indent = None
|
||||
reindent = ' '
|
||||
|
||||
def minify(self):
|
||||
return self.with_header()
|
||||
@@ -1399,12 +1396,25 @@ class SassAsset(StylesheetAsset):
|
||||
name=url,
|
||||
url=url,
|
||||
), context=self.context)
|
||||
return super(SassAsset, self).to_html()
|
||||
return super(PreprocessedCSS, self).to_html()
|
||||
|
||||
def get_source(self):
|
||||
content = self.inline or self._fetch_content()
|
||||
return "/*! %s */\n%s" % (self.id, content)
|
||||
|
||||
def get_command(self):
|
||||
raise NotImplementedError
|
||||
|
||||
class SassStylesheetAsset(PreprocessedCSS):
|
||||
rx_indent = re.compile(r'^( +|\t+)', re.M)
|
||||
indent = None
|
||||
reindent = ' '
|
||||
|
||||
def get_source(self):
|
||||
content = textwrap.dedent(self.inline or self._fetch_content())
|
||||
|
||||
def fix_indent(m):
|
||||
# Indentation normalization
|
||||
ind = m.group()
|
||||
if self.indent is None:
|
||||
self.indent = ind
|
||||
@@ -1419,6 +1429,16 @@ class SassAsset(StylesheetAsset):
|
||||
pass
|
||||
return "/*! %s */\n%s" % (self.id, content)
|
||||
|
||||
def get_command(self):
|
||||
return ['sass', '--stdin', '-t', 'compressed', '--unix-newlines', '--compass',
|
||||
'-r', 'bootstrap-sass']
|
||||
|
||||
class LessStylesheetAsset(PreprocessedCSS):
|
||||
def get_command(self):
|
||||
webpath = openerp.http.addons_manifest['web']['addons_path']
|
||||
lesspath = os.path.join(webpath, 'web', 'static', 'lib', 'bootstrap', 'less')
|
||||
return ['lessc', '-', '--clean-css', '--no-js', '--no-color', '--include-path=%s' % lesspath]
|
||||
|
||||
def rjsmin(script):
|
||||
""" Minify js with a clever regex.
|
||||
Taken from http://opensource.perlig.de/rjsmin
|
||||
|
||||
Reference in New Issue
Block a user