diff --git a/addons/website/controllers/main.py b/addons/website/controllers/main.py
index 5d1dbe04bec..624daaff31d 100644
--- a/addons/website/controllers/main.py
+++ b/addons/website/controllers/main.py
@@ -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:
diff --git a/addons/website/models/ir_ui_view.py b/addons/website/models/ir_ui_view.py
index ad3597b7309..61de01c4b96 100644
--- a/addons/website/models/ir_ui_view.py
+++ b/addons/website/models/ir_ui_view.py
@@ -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:
diff --git a/addons/website/static/src/css/editor.css b/addons/website/static/src/css/editor.css
index d38f26fd332..ac7ddbbccb5 100644
--- a/addons/website/static/src/css/editor.css
+++ b/addons/website/static/src/css/editor.css
@@ -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;
+}
diff --git a/addons/website/static/src/css/editor.sass b/addons/website/static/src/css/editor.sass
index b40907bc66c..802b6299545 100644
--- a/addons/website/static/src/css/editor.sass
+++ b/addons/website/static/src/css/editor.sass
@@ -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:
diff --git a/addons/website/static/src/js/website.ace.js b/addons/website/static/src/js/website.ace.js
index 1fb0b93c830..03c6747ddd1 100644
--- a/addons/website/static/src/js/website.ace.js
+++ b/addons/website/static/src/js/website.ace.js
@@ -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; }
diff --git a/addons/website/static/src/xml/website.ace.xml b/addons/website/static/src/xml/website.ace.xml
index 23f3ca0c2e4..28b9dd83192 100644
--- a/addons/website/static/src/xml/website.ace.xml
+++ b/addons/website/static/src/xml/website.ace.xml
@@ -6,6 +6,9 @@
+
diff --git a/openerp/addons/base/ir/ir_qweb.py b/openerp/addons/base/ir/ir_qweb.py
index 30763a655df..f54ad99385c 100644
--- a/openerp/addons/base/ir/ir_qweb.py
+++ b/openerp/addons/base/ir/ir_qweb.py
@@ -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 '')
return error
@@ -1373,11 +1373,8 @@ class StylesheetAsset(WebAsset):
else:
return '' % (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