[MERGE] Merge parents

bzr revid: rim@openerp.com-20131219081730-y57n3quz5j71hqux
This commit is contained in:
Richard Mathot (OpenERP) 2013-12-19 09:17:30 +01:00
commit 57dfcc6e12
13 changed files with 228 additions and 66 deletions

View File

@ -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?

View File

@ -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

View File

@ -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 {

View File

@ -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

View File

@ -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;

View File

@ -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);

View File

@ -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;

View File

@ -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, ]

View File

@ -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"
)

View File

@ -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

View File

@ -0,0 +1,4 @@
console.log('{ "event": "success" }');
// For a failure:
// console.log('{ "event": "failure", "message": "The test failed" }');
phantom.exit();

View File

@ -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>&amp;#x2706; <span t-field="res_company.phone"></span></div>
<div class="fa fa-envelope" t-field="res_company.email"></div>

View File

@ -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>