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:
Fabien Meghazi
2014-07-11 13:52:24 +02:00
7 changed files with 115 additions and 76 deletions
+3 -2
View File
@@ -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:
+7 -4
View File
@@ -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:
+15 -15
View File
@@ -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:
+29 -21
View File
@@ -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>
+54 -34
View File
@@ -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