diff --git a/addons/website/models/ir_qweb.py b/addons/website/models/ir_qweb.py
index 64a374119fa..32e6a8d33e8 100644
--- a/addons/website/models/ir_qweb.py
+++ b/addons/website/models/ir_qweb.py
@@ -204,12 +204,29 @@ class ManyToOne(orm.AbstractModel):
_inherit = ['website.qweb.field', 'ir.qweb.field.many2one']
def from_html(self, cr, uid, model, column, element, context=None):
- # FIXME: this behavior is really weird, what if the user wanted to edit the name of the related thingy? Should m2os really be editable without a widget?
- matches = self.pool[column._obj].name_search(
- cr, uid, name=element.text_content().strip(), context=context)
- # FIXME: no match? More than 1 match?
- assert len(matches) == 1
- return matches[0][0]
+ # FIXME: layering violations all the things
+ Model = self.pool[element.get('data-oe-model')]
+ M2O = self.pool[column._obj]
+ field = element.get('data-oe-field')
+ id = int(element.get('data-oe-id'))
+ value = element.text_content().strip()
+
+ # if anything blows up, just ignore it and bail
+ try:
+ # get parent record
+ [obj] = Model.read(cr, uid, [id], [field])
+ # get m2o record id
+ (m2o_id, _) = obj[field]
+ # assume _rec_name and write directly to it
+ M2O.write(cr, uid, [m2o_id], {
+ M2O._rec_name: value
+ }, context=context)
+ except:
+ logger.exception("Could not save %r to m2o field %s of model %s",
+ value, field, Model._name)
+
+ # not necessary, but might as well be explicit about it
+ return None
class HTML(orm.AbstractModel):
_name = 'website.qweb.field.html'
@@ -281,7 +298,7 @@ class Image(orm.AbstractModel):
match.group('module'), 'static', *(rest.split('/')))
if not path:
- return False
+ return None
try:
with open(path, 'rb') as f:
@@ -292,7 +309,7 @@ class Image(orm.AbstractModel):
return f.read().encode('base64')
except Exception:
logger.exception("Failed to load local image %r", url)
- return False
+ return None
def load_remote_url(self, url):
try:
@@ -310,7 +327,7 @@ class Image(orm.AbstractModel):
image.load()
except Exception:
logger.exception("Failed to load remote image %r", url)
- return False
+ return None
# don't use original data in case weird stuff was smuggled in, with
# luck PIL will remove some of it?
diff --git a/addons/website/models/ir_ui_view.py b/addons/website/models/ir_ui_view.py
index b935d6e3465..02fa87e10aa 100644
--- a/addons/website/models/ir_ui_view.py
+++ b/addons/website/models/ir_ui_view.py
@@ -70,9 +70,11 @@ class view(osv.osv):
el.get('data-oe-type'))
value = converter.from_html(cr, uid, Model, column, el)
- Model.write(cr, uid, [int(el.get('data-oe-id'))], {
- field: value
- }, context=context)
+ if value is not None:
+ # TODO: batch writes?
+ Model.write(cr, uid, [int(el.get('data-oe-id'))], {
+ field: value
+ }, context=context)
def to_field_ref(self, cr, uid, el, context=None):
# filter out meta-information inserted in the document
diff --git a/addons/website/static/src/css/editor.css b/addons/website/static/src/css/editor.css
index e21bfc1fbe8..b784177fd1c 100644
--- a/addons/website/static/src/css/editor.css
+++ b/addons/website/static/src/css/editor.css
@@ -1,4 +1,4 @@
-/* ---- CKEditor Minimal Reset ---- */
+/* ---- CKEditor Minimal Reset ---- {{{ */
.navbar.navbar-inverse .cke_chrome {
border: none;
}
@@ -28,7 +28,7 @@
box-shadow: none;
}
-/* ---- OpenERP Style ---- */
+/* ---- OpenERP Style ---- {{{ */
.oe_website_editorbar {
position: fixed;
top: 0;
@@ -103,7 +103,7 @@
top: 2px;
}
-/* ---- EDITOR BAR ---- */
+/* ---- EDITOR BAR ---- {{{ */
table.editorbar-panel {
cursor: pointer;
width: 100%;
@@ -115,6 +115,7 @@ table.editorbar-panel td.selected {
background-color: #b1c9d9;
}
+/* ---- TRANSLATIONS ---- {{{ */
.oe_translate_or {
color: white;
padding: 0 0 0 1em;
@@ -145,6 +146,7 @@ table.editorbar-panel td.selected {
background: #ffffb6;
}
+/* ---- MENU ---- {{{ */
div.oe_menu_buttons {
top: -8px;
right: -8px;
@@ -171,11 +173,12 @@ ul.oe_menu_editor .disclose {
display: none;
}
-/* ---- RTE ---- */
+/* ---- RTE ---- {{{ */
.oe_editable .btn, .btn.oe_editable {
-webkit-user-select: auto;
-moz-user-select: auto;
user-select: auto;
+ cursor: text !important;
}
.modal-dialog.select-image {
@@ -233,12 +236,11 @@ ul.oe_menu_editor .disclose {
z-index: 1000;
}
-/* ---- SNIPPET EDITOR ---- */
+/* ---- SNIPPET EDITOR ---- {{{ */
#oe_snippets {
position: fixed;
left: 0px;
right: 0px;
- top: 51px;
background: #282828;
-webkit-box-shadow: 0px 10px 10px -10px black inset;
-moz-box-shadow: 0px 10px 10px -10px black inset;
@@ -368,7 +370,7 @@ ul.oe_menu_editor .disclose {
background: #747474, radial-gradient(rgba(0, 0, 0, 0.25), rgba(0, 0, 0, 0.4));
}
-/* ---- SNIPPETS DROP ZONES ---- */
+/* ---- SNIPPETS DROP ZONES ---- {{{ */
.oe_drop_zone.oe_insert {
display: block;
height: 48px;
@@ -429,7 +431,7 @@ ul.oe_menu_editor .disclose {
padding-top: 12px;
}
-/* ---- SNIPPET MANIPULATOR ---- */
+/* ---- SNIPPET MANIPULATOR ---- {{{ */
.oe_overlay {
display: none;
position: absolute;
@@ -599,7 +601,7 @@ ul.oe_menu_editor .disclose {
cursor: move !important;
}
-/* ---- MOBILE PREVIEW ---- */
+/* ---- MOBILE PREVIEW ---- {{{ */
.oe_mobile_preview.modal .modal-content {
height: 660px;
background-color: black;
@@ -655,7 +657,7 @@ ul.oe_menu_editor .disclose {
background-color: black;
}
-/* ---- SEO TOOLS ---- */
+/* ---- SEO TOOLS ---- {{{ */
.oe_seo_configuration .modal-dialog {
width: 80%;
}
@@ -674,10 +676,9 @@ ul.oe_menu_editor .disclose {
border-radius: 0.4em;
}
-/* ---- ACE EDITOR ---- */
+/* ---- ACE EDITOR ---- {{{ */
.oe_ace_view_editor {
position: fixed;
- top: 51px;
right: 0;
z-index: 1000;
height: 100%;
@@ -707,7 +708,6 @@ ul.oe_menu_editor .disclose {
position: absolute;
top: 30px;
right: 0;
- bottom: 51px;
left: 0;
}
.oe_ace_view_editor .ace_editor .ace_gutter {
@@ -723,7 +723,7 @@ ul.oe_menu_editor .disclose {
opacity: 0;
}
-/* ---- EDITOR TOUR ---- */
+/* ---- EDITOR TOUR ---- {{{ */
div.tour-backdrop {
z-index: 2009;
}
diff --git a/addons/website/static/src/css/editor.sass b/addons/website/static/src/css/editor.sass
index ff62f416d8d..6cc590bf6b2 100644
--- a/addons/website/static/src/css/editor.sass
+++ b/addons/website/static/src/css/editor.sass
@@ -1,8 +1,6 @@
-@charset "utf-8"
-
@import "common"
-/* ---- CKEditor Minimal Reset ---- */
+/* ---- CKEditor Minimal Reset ---- {{{ */
.navbar.navbar-inverse .cke_chrome
border: none
@@ -26,7 +24,9 @@
border: none
+box-shadow(none)
-/* ---- OpenERP Style ---- */
+// }}}
+
+/* ---- OpenERP Style ---- {{{ */
.oe_website_editorbar
position: fixed
@@ -85,7 +85,9 @@
position: relative !important
top: +2px
-/* ---- EDITOR BAR ---- */
+// }}}
+
+/* ---- EDITOR BAR ---- {{{ */
table.editorbar-panel
cursor: pointer
@@ -95,7 +97,10 @@ table.editorbar-panel
td.selected
background-color: #b1c9d9
-// ---- TRANSLATIONS ---- {{{
+// }}}
+
+/* ---- TRANSLATIONS ---- {{{ */
+
.oe_translate_or
color: white
padding: 0 0 0 1em
@@ -112,9 +117,11 @@ table.editorbar-panel
padding: 0 10px
.oe_translatable_todo
background: rgb(255, 255, 182)
+
// }}}
-// -------- MENU -------- {{{
+/* ---- MENU ---- {{{ */
+
div.oe_menu_buttons
top: -8px
right: -8px
@@ -137,12 +144,13 @@ ul.oe_menu_editor
// }}}
-/* ---- RTE ---- */
+/* ---- RTE ---- {{{ */
// bootstrap makes .btn elements unselectable -> RTE double-click can't know
// about them either
.oe_editable .btn, .btn.oe_editable
+user-select(auto)
+ cursor: text !important
.modal-dialog.select-image
width: 80%
@@ -197,13 +205,15 @@ ul.oe_menu_editor
// This z-index is due to .navbar of bootstrap
z-index: 1000
-/* ---- SNIPPET EDITOR ---- */
+// }}}
+
+/* ---- SNIPPET EDITOR ---- {{{ */
#oe_snippets
position: fixed
left: 0px
right: 0px
- top: 51px
+ // top property is set programmatically
background: rgb(40,40,40)
+box-shadow(0px 10px 10px -10px black inset)
z-index: 1010
@@ -286,7 +296,9 @@ ul.oe_menu_editor
#oe_snippets .oe_snippet_thumbnail
@include background(#747474, radial-gradient(rgba(0,0,0,0.25),rgba(0,0,0,0.4)))
-/* ---- SNIPPETS DROP ZONES ---- */
+// }}}}
+
+/* ---- SNIPPETS DROP ZONES ---- {{{ */
.oe_drop_zone.oe_insert
display: block
@@ -333,7 +345,9 @@ ul.oe_menu_editor
margin-bottom: 32px
padding-top: 12px
-/* ---- SNIPPET MANIPULATOR ---- */
+// }}}
+
+/* ---- SNIPPET MANIPULATOR ---- {{{ */
.oe_overlay
display: none
@@ -454,8 +468,9 @@ ul.oe_menu_editor
.move-important, .move-important *
cursor: move !important
+// }}}
-/* ---- MOBILE PREVIEW ---- */
+/* ---- MOBILE PREVIEW ---- {{{ */
$mobile_preview_background: #000000
$mobile_preview_border: #1C1F1F
@@ -498,7 +513,9 @@ $icon_close: #E00101
.modal-footer
background-color: $mobile_preview_background
-/* ---- SEO TOOLS ---- */
+// }}}
+
+/* ---- SEO TOOLS ---- {{{ */
$remove_color: $icon_close
$in_title_color: #5cb85c
@@ -516,15 +533,16 @@ $highlighted_text_color: #ffffff
padding: .2em .4em .2em .5em
+border-radius(.4em)
-/* ---- ACE EDITOR ---- */
+// }}}
+
+/* ---- ACE EDITOR ---- {{{ */
$editorbar_height: 30px
// TODO Fix => might break with themes
-$navbar_height: 51px
.oe_ace_view_editor
position: fixed
- top: $navbar_height
+ // top property is set programmatically
right: 0
z-index: 1000
height: 100%
@@ -546,7 +564,7 @@ $navbar_height: 51px
position: absolute
top: $editorbar_height
right: 0
- bottom: $navbar_height
+ // bottom property is set programmatically
left: 0
.ace_gutter
cursor: ew-resize
@@ -556,9 +574,15 @@ $navbar_height: 51px
z-index: -1000
+opacity(0)
-/* ---- EDITOR TOUR ---- */
+// }}}
+
+/* ---- EDITOR TOUR ---- {{{ */
+
div.tour-backdrop
z-index: 2009
.popover.tour
z-index: 2010
+
+// }}}
+
// 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 7108fd9ded7..f3b8a5c5885 100644
--- a/addons/website/static/src/js/website.ace.js
+++ b/addons/website/static/src/js/website.ace.js
@@ -1,35 +1,34 @@
(function () {
'use strict';
- var globalEditor;
-
var hash = "#advanced-view-editor";
var website = openerp.website;
website.add_template_file('/website/static/src/xml/website.ace.xml');
- website.ready().then(function () {
- if (window.location.hash.indexOf(hash) >= 0) {
- launch();
- }
- });
-
- function launch () {
- if (globalEditor) {
- globalEditor.open();
- } else {
- globalEditor = new website.ace.ViewEditor();
- globalEditor.appendTo($(document.body));
- }
- }
-
website.EditorBar.include({
events: _.extend({}, website.EditorBar.prototype.events, {
'click a[data-action=ace]': 'launchAce',
}),
+ start: function () {
+ var self = this;
+ this.globalEditor = null;
+ return this._super.apply(this, arguments).then(function () {
+ if (window.location.hash.indexOf(hash) >= 0) {
+ self.launchAce();
+ }
+ });
+ },
launchAce: function (e) {
- e.preventDefault();
- launch();
+ if (e) {
+ e.preventDefault();
+ }
+ if (this.globalEditor) {
+ this.globalEditor.open();
+ } else {
+ this.globalEditor = new website.ace.ViewEditor(this);
+ this.globalEditor.appendTo($(document.body));
+ }
},
});
@@ -104,6 +103,10 @@
self.aceEditor.resize();
self.$el.width(width);
}
+ function resizeEditorHeight(height) {
+ self.$el.css('top', height);
+ self.$('.ace_editor').css('bottom', height);
+ }
function storeEditorWidth() {
window.localStorage.setItem('ace_editor_width', self.$el.width());
}
@@ -133,7 +136,11 @@
$('button[data-action=edit]').click(function () {
self.close();
});
+ this.getParent().on('change:height', this, function (editor) {
+ resizeEditorHeight(editor.get('height'));
+ });
resizeEditor(readEditorWidth());
+ resizeEditorHeight(this.getParent().get('height'));
},
loadViews: function (views) {
var self = this;
diff --git a/addons/website/static/src/js/website.editor.js b/addons/website/static/src/js/website.editor.js
index c7c5620c9a4..51f14a0f6d5 100644
--- a/addons/website/static/src/js/website.editor.js
+++ b/addons/website/static/src/js/website.editor.js
@@ -374,7 +374,6 @@
editor.edit();
}
});
- $body.css('padding-top', '50px'); // Not working properly: editor.$el.outerHeight());
};
/* ----- TOP EDITOR BAR FOR ADMIN ---- */
@@ -423,7 +422,6 @@
},
start: function() {
var self = this;
-
this.saving_mutex = new openerp.Mutex();
this.$('#website-top-edit').hide();
@@ -443,12 +441,25 @@
this.rte.on('rte:ready', this, function () {
self.setup_hover_buttons();
self.trigger('rte:ready');
+ self.check_height();
});
+ $(window).on('resize', _.debounce(this.check_height.bind(this), 50));
+ this.check_height();
+
return $.when(
this._super.apply(this, arguments),
this.rte.appendTo(this.$('#website-top-edit .nav.pull-right'))
- );
+ ).then(function () {
+ self.check_height();
+ });
+ },
+ check_height: function () {
+ var editor_height = this.$el.outerHeight();
+ if (this.get('height') != editor_height) {
+ $(document.body).css('padding-top', editor_height);
+ this.set('height', editor_height);
+ }
},
edit: function () {
this.$buttons.edit.prop('disabled', true);
@@ -456,7 +467,7 @@
this.$('#website-top-edit').show();
$('.css_non_editable_mode_hidden').removeClass("css_non_editable_mode_hidden");
- this.rte.start_edition();
+ this.rte.start_edition().then(this.check_height.bind(this));
},
rte_changed: function () {
this.$buttons.save.prop('disabled', false);
diff --git a/addons/website/static/src/js/website.snippets.editor.js b/addons/website/static/src/js/website.snippets.editor.js
index 4cf49bb962b..d4f8920663a 100644
--- a/addons/website/static/src/js/website.snippets.editor.js
+++ b/addons/website/static/src/js/website.snippets.editor.js
@@ -139,6 +139,10 @@
$(document).on('click', '.dropdown-submenu a[tabindex]', function (e) {
e.preventDefault();
});
+
+ this.getParent().on('change:height', this, function (editor) {
+ self.$el.css('top', editor.get('height'));
+ });
},
fetch_snippet_templates: function () {
var self = this;
diff --git a/addons/website/tests/__init__.py b/addons/website/tests/__init__.py
index 24de4d7ddad..665bbc9f535 100644
--- a/addons/website/tests/__init__.py
+++ b/addons/website/tests/__init__.py
@@ -1,4 +1,4 @@
# -*- coding: utf-8 -*-
-import test_views, test_converter, test_requests
+import test_views, test_converter, test_requests, test_ui
checks = [ test_views, test_converter, ]
diff --git a/addons/website/tests/test_converter.py b/addons/website/tests/test_converter.py
index 1086ec8e8d3..4b24e8f0b9f 100644
--- a/addons/website/tests/test_converter.py
+++ b/addons/website/tests/test_converter.py
@@ -63,20 +63,6 @@ class TestConvertBack(common.TransactionCase):
self.field_roundtrip('char', "ⒸⓄⓇⒼⒺ")
- def test_m2o(self):
- Sub = self.registry('website.converter.test.sub')
- sub = partial(Sub.create, self.cr, self.uid)
- ids = [
- sub({'name': "Foo"}),
- sub({'name': "Bar"}),
- sub({'name': "Baz"}),
- ]
-
- self.field_rountrip_result(
- 'many2one',
- ids[2],
- ids[2])
-
def test_selection(self):
self.field_roundtrip('selection', 3)
@@ -104,3 +90,47 @@ class TestConvertBack(common.TransactionCase):
You never know
You never know until you go
""")
+
+ def test_m2o(self):
+ """ the M2O field conversion (from html) is markedly different from
+ others as it directly writes into the m2o and returns nothing at all.
+ """
+ model = 'website.converter.test'
+ field = 'many2one'
+
+ Sub = self.registry('website.converter.test.sub')
+ sub_id = Sub.create(self.cr, self.uid, {'name': "Foo"})
+
+ Model = self.registry(model)
+ id = Model.create(self.cr, self.uid, {field: sub_id})
+ [record] = Model.browse(self.cr, self.uid, [id])
+
+ e = document.createElement('span')
+ field_value = 'record.%s' % field
+ e.setAttribute('t-field', field_value)
+
+ rendered = self.registry('website.qweb').render_tag_field(
+ e, {'field': field_value}, '', ir_qweb.QWebContext(self.cr, self.uid, {
+ 'record': record,
+ }))
+
+ element = html.fromstring(rendered, parser=html.HTMLParser(encoding='utf-8'))
+ # emulate edition
+ element.text = "New content"
+
+ column = Model._all_columns[field].column
+ converter = self.registry('website.qweb').get_converter_for(
+ element.get('data-oe-type'))
+
+ value_back = converter.from_html(
+ self.cr, self.uid, model, column, element)
+
+ self.assertIsNone(
+ value_back, "the m2o converter should return None to avoid spurious"
+ " or useless writes on the parent record")
+
+ self.assertEqual(
+ Sub.browse(self.cr, self.uid, sub_id).name,
+ "New content",
+ "element edition should have been written directly to the m2o record"
+ )
diff --git a/addons/website/tests/test_ui.py b/addons/website/tests/test_ui.py
new file mode 100644
index 00000000000..927d407af70
--- /dev/null
+++ b/addons/website/tests/test_ui.py
@@ -0,0 +1,98 @@
+import json
+import subprocess
+import unittest
+import os
+import time
+
+ROOT = os.path.join(os.path.dirname(__file__), 'ui_suite')
+
+__all__ = ['load_tests', 'WebsiteUiSuite']
+
+def _exc_info_to_string(err, test):
+ return err
+
+class WebsiteUiTest(unittest.TestCase):
+ def __init__(self, name):
+ self.name = name
+ def shortDescription(self):
+ return None
+ def __str__(self):
+ return self.name
+
+class WebsiteUiSuite(unittest.TestSuite):
+ def __init__(self, testfile, timeout=5000):
+ self.testfile = testfile
+ self.timeout = timeout
+ self._test = None
+
+ def __iter__(self):
+ return iter([self])
+
+ def run(self, result):
+ # Test if phantom is correctly installed
+ try:
+ subprocess.call(['phantomjs', '-v'],
+ stdout=open(os.devnull, 'w'),
+ stderr=subprocess.STDOUT)
+ except OSError:
+ test = WebsiteUiTest('UI Tests')
+ result.startTest(test)
+ result.addSkip(test, "phantomjs command not found")
+ result.stopTest(test)
+ return
+
+ result._exc_info_to_string = _exc_info_to_string
+ try:
+ self._run(result)
+ finally:
+ del result._exc_info_to_string
+
+ def _run(self, result):
+ phantom = subprocess.Popen([
+ 'phantomjs',
+ os.path.join(ROOT, self.testfile),
+ json.dumps({ 'timeout': self.timeout })
+ ], stdout=subprocess.PIPE)
+
+ self._test = WebsiteUiTest(self.testfile)
+
+ try:
+ while True:
+ line = phantom.stdout.readline()
+ if line:
+ # the runner expects only one result per test
+ self.process(line, result)
+ result.stopTest(self._test)
+ break
+ else:
+ time.sleep(0.1)
+ finally:
+ # kill phantomjs if phantom.exit() wasn't called in the test
+ if phantom.poll() is None:
+ phantom.terminate()
+
+ def process(self, line, result):
+ # Test protocol
+ # -------------
+ # use console.log in phantomjs to output test results using the following format:
+ # - for a success: { "event": "success" }
+ # - for a failure: { "event": "failure", "message": "Failure description" }
+ # any other message is treated as an error
+ result.startTest(self._test)
+ try:
+ args = json.loads(line)
+ event = args.get('event')
+ message = args.get('message', "")
+
+ if event == 'success':
+ result.addSuccess(self._test)
+ elif event == 'failure':
+ result.addFailure(self._test, message)
+ else:
+ result.addError(self._test, "Unexpected message: %s" % line)
+ except ValueError:
+ result.addError(self._test, "Unexpected message: %s" % line)
+
+def load_tests(loader, base, _):
+ base.addTest(WebsiteUiSuite('sample_test.js'))
+ return base
\ No newline at end of file
diff --git a/addons/website/tests/ui_suite/sample_test.js b/addons/website/tests/ui_suite/sample_test.js
new file mode 100644
index 00000000000..7646e4ec76a
--- /dev/null
+++ b/addons/website/tests/ui_suite/sample_test.js
@@ -0,0 +1,4 @@
+console.log('{ "event": "success" }');
+// For a failure:
+// console.log('{ "event": "failure", "message": "The test failed" }');
+phantom.exit();
\ No newline at end of file
diff --git a/addons/website/views/website_templates.xml b/addons/website/views/website_templates.xml
index fa65b909c9a..4272bdcf1bd 100644
--- a/addons/website/views/website_templates.xml
+++ b/addons/website/views/website_templates.xml
@@ -597,7 +597,7 @@ Sitemap: sitemap.xml
- Name
+ Name
✆
diff --git a/addons/website_blog/data/website_blog_data.xml b/addons/website_blog/data/website_blog_data.xml
index e75281172b4..9bb8ff39e1b 100644
--- a/addons/website_blog/data/website_blog_data.xml
+++ b/addons/website_blog/data/website_blog_data.xml
@@ -52,12 +52,5 @@
-
-
- Jobs
- Job Announces
-
-
-