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