[MERGE] Merge parents
bzr revid: rim@openerp.com-20131219081730-y57n3quz5j71hqux
This commit is contained in:
commit
57dfcc6e12
|
@ -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?
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -238,7 +238,6 @@ ul.oe_menu_editor .disclose {
|
|||
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;
|
||||
|
@ -677,7 +676,6 @@ ul.oe_menu_editor .disclose {
|
|||
/* ---- ACE EDITOR ---- */
|
||||
.oe_ace_view_editor {
|
||||
position: fixed;
|
||||
top: 51px;
|
||||
right: 0;
|
||||
z-index: 1000;
|
||||
height: 100%;
|
||||
|
@ -707,7 +705,6 @@ ul.oe_menu_editor .disclose {
|
|||
position: absolute;
|
||||
top: 30px;
|
||||
right: 0;
|
||||
bottom: 51px;
|
||||
left: 0;
|
||||
}
|
||||
.oe_ace_view_editor .ace_editor .ace_gutter {
|
||||
|
|
|
@ -203,7 +203,7 @@ ul.oe_menu_editor
|
|||
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
|
||||
|
@ -520,11 +520,10 @@ $highlighted_text_color: #ffffff
|
|||
|
||||
$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 +545,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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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, ]
|
||||
|
|
|
@ -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"
|
||||
)
|
||||
|
|
|
@ -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
|
|
@ -0,0 +1,4 @@
|
|||
console.log('{ "event": "success" }');
|
||||
// For a failure:
|
||||
// console.log('{ "event": "failure", "message": "The test failed" }');
|
||||
phantom.exit();
|
|
@ -13,7 +13,7 @@
|
|||
|
||||
<template id="website.submenu" name="Submenu">
|
||||
<li t-if="not submenu.child_id">
|
||||
<a t-att-href="url_for(submenu.url)" t-ignore="true">
|
||||
<a t-att-href="url_for(submenu.url)" t-ignore="true" t-att-target="'blank' if submenu.new_window else None">
|
||||
<span t-field="submenu.name"/>
|
||||
</a>
|
||||
</li>
|
||||
|
@ -598,7 +598,7 @@ Sitemap: <t t-esc="url_root"/>sitemap.xml
|
|||
|
||||
<template id="company_description" name="Company Description">
|
||||
<address>
|
||||
<div t-field="res_company.partner_id">Name</div>
|
||||
<div t-field="res_company.name">Name</div>
|
||||
<br />
|
||||
<div>&#x2706; <span t-field="res_company.phone"></span></div>
|
||||
<div class="fa fa-envelope" t-field="res_company.email"></div>
|
||||
|
|
|
@ -52,12 +52,5 @@
|
|||
</record>
|
||||
|
||||
|
||||
<!-- Mail group for the company's jobs -->
|
||||
<record id="blog_category_2" model="blog.category">
|
||||
<field name="name">Jobs</field>
|
||||
<field name="description">Job Announces</field>
|
||||
</record>
|
||||
|
||||
|
||||
</data>
|
||||
</openerp>
|
||||
|
|
Loading…
Reference in New Issue