[MERGE] from trunk

bzr revid: chm@openerp.com-20130320130931-0mk35kjf5e3g8fnd
This commit is contained in:
Christophe Matthieu 2013-03-20 14:09:31 +01:00
commit 5ad1ecb48f
35 changed files with 1724 additions and 542 deletions

View File

@ -26,6 +26,7 @@ This module provides the core of the OpenERP Web Client.
"static/lib/spinjs/spin.js",
"static/lib/jquery.autosize/jquery.autosize.js",
"static/lib/jquery.blockUI/jquery.blockUI.js",
"static/lib/jquery.placeholder/jquery.placeholder.js",
"static/lib/jquery.ui/js/jquery-ui-1.9.1.custom.js",
"static/lib/jquery.ui.timepicker/js/jquery-ui-timepicker-addon.js",
"static/lib/jquery.ui.notify/js/jquery.notify.js",

View File

@ -13,7 +13,10 @@ import os
import re
import simplejson
import time
import urllib
import urllib2
import urlparse
import xmlrpclib
import zlib
from xml.etree import ElementTree
from cStringIO import StringIO
@ -90,16 +93,50 @@ def db_list(req):
dbs = [i for i in dbs if re.match(r, i)]
return dbs
def db_monodb(req):
# if only one db exists, return it else return False
def db_monodb_redirect(req):
db = False
redirect = False
# 1 try the db in the url
db_url = req.params.get('db')
if db_url:
return (db_url, False)
try:
dbs = db_list(req)
if len(dbs) == 1:
return dbs[0]
except Exception:
# ignore access denied
pass
return False
dbs = []
# 2 use the database from the cookie if it's listable and still listed
cookie_db = req.httprequest.cookies.get('last_used_database')
if cookie_db in dbs:
db = cookie_db
# 3 use the first db
if dbs and not db:
db = dbs[0]
# redirect to the chosen db if multiple are available
if db and len(dbs) > 1:
query = dict(urlparse.parse_qsl(req.httprequest.query_string, keep_blank_values=True))
query.update({ 'db': db })
redirect = req.httprequest.path + '?' + urllib.urlencode(query)
return (db, redirect)
def db_monodb(req):
# if only one db exists, return it else return False
return db_monodb_redirect(req)[0]
def redirect_with_hash(req, url, code=303):
if req.httprequest.user_agent.browser == 'msie':
try:
version = float(req.httprequest.user_agent.version)
if version < 10:
return "<html><head><script>window.location = '%s#' + location.hash;</script></head></html>" % url
except Exception:
pass
return werkzeug.utils.redirect(url, code)
def module_topological_sort(modules):
""" Return a list of module names sorted so that their dependencies of the
@ -290,20 +327,19 @@ def manifest_glob(req, extension, addons=None, db=None):
return r
def manifest_list(req, extension, mods=None, db=None):
""" list ressources to load specifying either:
mods: a comma separated string listing modules
db: a database name (return all installed modules in that database)
"""
if not req.debug:
path = '/web/webclient/' + extension
if mods is not None:
path += '?mods=' + mods
path += '?' + urllib.urlencode({'mods': mods})
elif db:
path += '?db=' + db
path += '?' + urllib.urlencode({'db': db})
return [path]
files = manifest_glob(req, extension, addons=mods, db=db)
i_am_diabetic = req.httprequest.environ["QUERY_STRING"].count("no_sugar") >= 1 or \
req.httprequest.environ.get('HTTP_REFERER', '').count("no_sugar") >= 1
if i_am_diabetic:
return [wp for _fp, wp in files]
else:
return ['%s?debug=%s' % (wp, os.path.getmtime(fp)) for fp, wp in files]
return [wp for _fp, wp in files]
def get_last_modified(files):
""" Returns the modification time of the most recently modified
@ -533,6 +569,10 @@ class Home(openerpweb.Controller):
@openerpweb.httprequest
def index(self, req, s_action=None, db=None, **kw):
db, redir = db_monodb_redirect(req)
if redir:
return redirect_with_hash(req, redir)
js = "\n ".join('<script type="text/javascript" src="%s"></script>' % i for i in manifest_list(req, 'js', db=db))
css = "\n ".join('<link rel="stylesheet" href="%s">' % i for i in manifest_list(req, 'css', db=db))
@ -683,7 +723,7 @@ class WebClient(openerpweb.Controller):
@openerpweb.jsonrequest
def version_info(self, req):
return openerp.service.web_services.RPC_VERSION_1
return openerp.service.common.exp_version()
class Proxy(openerpweb.Controller):
_cp_path = '/web/proxy'
@ -868,7 +908,7 @@ class Session(openerpweb.Controller):
"""
saved_actions = req.httpsession.get('saved_actions')
if not saved_actions:
saved_actions = {"next":0, "actions":{}}
saved_actions = {"next":1, "actions":{}}
req.httpsession['saved_actions'] = saved_actions
# we don't allow more than 10 stored actions
if len(saved_actions["actions"]) >= 10:
@ -1310,7 +1350,7 @@ class Binary(openerpweb.Controller):
'id': attachment_id
}
except Exception:
args = {'error':e.faultCode }
args = {'error': "Something horrible happened"}
return out % (simplejson.dumps(callback), simplejson.dumps(args))
@openerpweb.httprequest

View File

@ -32,12 +32,12 @@ NOMODULE_TEMPLATE = Template(u"""<!DOCTYPE html>
</form>
</body>
</html>
""")
""", default_filters=['h'])
NOTFOUND = Template(u"""
<p>Unable to find the module [${module}], please check that the module
name is correct and the module is on OpenERP's path.</p>
<a href="/web/tests">&lt;&lt; Back to tests</a>
""")
""", default_filters=['h'])
TESTING = Template(u"""<!DOCTYPE html>
<html style="height: 100%">
<%def name="to_path(module, p)">/${module}/${p}</%def>
@ -51,9 +51,9 @@ TESTING = Template(u"""<!DOCTYPE html>
<script src="/web/static/lib/qunit/qunit.js"></script>
<script type="text/javascript">
var oe_db_info = ${db_info};
var oe_db_info = ${db_info | n};
// List of modules, each module is preceded by its dependencies
var oe_all_dependencies = ${dependencies};
var oe_all_dependencies = ${dependencies | n};
QUnit.config.testTimeout = 5 * 60 * 1000;
</script>
</head>
@ -83,7 +83,7 @@ TESTING = Template(u"""<!DOCTYPE html>
% endif
% endfor
</html>
""")
""", default_filters=['h'])
class TestRunnerController(http.Controller):
_cp_path = '/web/tests'

View File

@ -107,6 +107,12 @@ formatted differently). If an input *may* fetch multiple completion
items, it *should* prefix those with a section title using its own
name. This has no technical consequence but is clearer for users.
.. note::
If a field is :js:func:`invisible
<openerp.web.search.Input.visible>`, its completion function will
*not* be called.
Providing drawer/supplementary UI
+++++++++++++++++++++++++++++++++
@ -145,6 +151,11 @@ started only once (per view).
dynamically collects, lays out and renders filters? =>
exercises drawer thingies
.. note::
An :js:func:`invisible <openerp.web.search.Input.visible>` input
will not be inserted into the drawer.
Converting from facet objects
+++++++++++++++++++++++++++++

View File

@ -92,6 +92,14 @@ class WebRequest(object):
if not self.session:
self.session = session.OpenERPSession()
self.httpsession[self.session_id] = self.session
# set db/uid trackers - they're cleaned up at the WSGI
# dispatching phase in openerp.service.wsgi_server.application
if self.session._db:
threading.current_thread().dbname = self.session._db
if self.session._uid:
threading.current_thread().uid = self.session._uid
self.context = self.params.pop('context', {})
self.debug = self.params.pop('debug', False) is not False
# Determine self.lang
@ -286,7 +294,8 @@ class HttpRequest(WebRequest):
_logger.debug("%s --> %s.%s %r", self.httprequest.method, method.im_class.__name__, method.__name__, akw)
try:
r = method(self, **self.params)
except Exception:
except Exception, e:
_logger.exception("An exception occured during an http request")
se = serialize_exception(e)
error = {
'code': 200,

View File

@ -38,6 +38,7 @@ class Model(object):
self.proxy = self.session.proxy('object')
def __getattr__(self, method):
self.session.assert_valid()
def proxy(*args, **kw):
result = self.proxy.execute_kw(self.session._db, self.session._uid, self.session._password, self.model, method, args, kw)
# reorder read
@ -109,8 +110,8 @@ class OpenERPSession(object):
if self._uid and not force:
return
# TODO use authenticate instead of login
uid = self.proxy("common").login(self._db, self._login, self._password)
if not uid:
self._uid = self.proxy("common").login(self._db, self._login, self._password)
if not self._uid:
raise AuthenticationError("Authentication failure")
def ensure_valid(self):
@ -121,7 +122,6 @@ class OpenERPSession(object):
self._uid = None
def execute(self, model, func, *l, **d):
self.assert_valid()
model = self.model(model)
r = getattr(model, func)(*l, **d)
return r

View File

@ -11,8 +11,8 @@ Date.CultureInfo = {
firstLetterDayNames: ["أ", "ا", "ث", "أ", "خ", "ج", "س"],
/* Month Name Strings */
monthNames: ["كانون الثاني", "شباط", "آذار", "نيسان", "أيار", "حزيران", "تموز", "آب", "أيلول", "تشرين الأول", "تشرين الثاني", "كانون الأول"],
abbreviatedMonthNames: ["كانون الثاني", "شباط", "آذار", "نيسان", "أيار", "حزيران", "تموز", "آب", "أيلول", "تشرين الأول", "تشرين الثاني", "كانون الأول"],
monthNames: ["يناير", "فبراير", "مارس", "أبريل", "مايو", "يونيو", "يوليو", "أغسطس", "سبتمبر", "أكتوبر", "نوفمبر", "ديسمبر"],
abbreviatedMonthNames: ["يناير", "فبراير", "مارس", "أبريل", "مايو", "يونيو", "يوليو", "أغسطس", "سبتمبر", "أكتوبر", "نوفمبر", "ديسمبر"],
/* AM/PM Designators */
amDesignator: "ص",
@ -82,18 +82,18 @@ Date.CultureInfo = {
* As well, please review the list of "Future Strings" section below.
*/
regexPatterns: {
jan: /^كانون الثاني/i,
feb: /^شباط/i,
mar: /^آذار/i,
apr: /^نيسان/i,
may: /^أيار/i,
jun: /^حزيران/i,
jul: /^تموز/i,
aug: /^آب/i,
sep: /^أيلول/i,
oct: /^تشرين الأول/i,
nov: /^تشرين الثاني/i,
dec: /^كانون الأول/i,
jan: /^يناير/i,
feb: /^فبراير/i,
mar: /^مارس/i,
apr: /^أبريل/i,
may: /^مايو/i,
jun: /^يونيو/i,
jul: /^يوليو/i,
aug: /^أغسطس/i,
sep: /^سبتمبر/i,
oct: /^أكتوبر/i,
nov: /^نوفمبر/i,
dec: /^ديسمبر/i,
sun: /^الاحد/i,
mon: /^ا(1)?/i,
@ -192,4 +192,4 @@ Date.CultureInfo = {
* end end
* long long
* short short
*/
*/

View File

@ -0,0 +1,157 @@
/*! http://mths.be/placeholder v2.0.7 by @mathias */
;(function(window, document, $) {
var isInputSupported = 'placeholder' in document.createElement('input'),
isTextareaSupported = 'placeholder' in document.createElement('textarea'),
prototype = $.fn,
valHooks = $.valHooks,
hooks,
placeholder;
if (isInputSupported && isTextareaSupported) {
placeholder = prototype.placeholder = function() {
return this;
};
placeholder.input = placeholder.textarea = true;
} else {
placeholder = prototype.placeholder = function() {
var $this = this;
$this
.filter((isInputSupported ? 'textarea' : ':input') + '[placeholder]')
.not('.placeholder')
.bind({
'focus.placeholder': clearPlaceholder,
'blur.placeholder': setPlaceholder
})
.data('placeholder-enabled', true)
.trigger('blur.placeholder');
return $this;
};
placeholder.input = isInputSupported;
placeholder.textarea = isTextareaSupported;
hooks = {
'get': function(element) {
var $element = $(element);
return $element.data('placeholder-enabled') && $element.hasClass('placeholder') ? '' : element.value;
},
'set': function(element, value) {
var $element = $(element);
if (!$element.data('placeholder-enabled')) {
return element.value = value;
}
if (value == '') {
element.value = value;
// Issue #56: Setting the placeholder causes problems if the element continues to have focus.
if (element != document.activeElement) {
// We can't use `triggerHandler` here because of dummy text/password inputs :(
setPlaceholder.call(element);
}
} else if ($element.hasClass('placeholder')) {
clearPlaceholder.call(element, true, value) || (element.value = value);
} else {
element.value = value;
}
// `set` can not return `undefined`; see http://jsapi.info/jquery/1.7.1/val#L2363
return $element;
}
};
isInputSupported || (valHooks.input = hooks);
isTextareaSupported || (valHooks.textarea = hooks);
$(function() {
// Look for forms
$(document).delegate('form', 'submit.placeholder', function() {
// Clear the placeholder values so they don't get submitted
var $inputs = $('.placeholder', this).each(clearPlaceholder);
setTimeout(function() {
$inputs.each(setPlaceholder);
}, 10);
});
});
// Clear placeholder values upon page reload
$(window).bind('beforeunload.placeholder', function() {
$('.placeholder').each(function() {
this.value = '';
});
});
}
function args(elem) {
// Return an object of element attributes
var newAttrs = {},
rinlinejQuery = /^jQuery\d+$/;
$.each(elem.attributes, function(i, attr) {
if (attr.specified && !rinlinejQuery.test(attr.name)) {
newAttrs[attr.name] = attr.value;
}
});
return newAttrs;
}
function clearPlaceholder(event, value) {
var input = this,
$input = $(input);
if (input.value == $input.attr('placeholder') && $input.hasClass('placeholder')) {
if ($input.data('placeholder-password')) {
$input = $input.hide().next().show().attr('id', $input.removeAttr('id').data('placeholder-id'));
// If `clearPlaceholder` was called from `$.valHooks.input.set`
if (event === true) {
return $input[0].value = value;
}
$input.focus();
} else {
input.value = '';
$input.removeClass('placeholder');
input == document.activeElement && input.select();
}
}
}
function setPlaceholder() {
var $replacement,
input = this,
$input = $(input),
$origInput = $input,
id = this.id;
if (input.value == '') {
if (input.type == 'password') {
if (!$input.data('placeholder-textinput')) {
try {
$replacement = $input.clone().attr({ 'type': 'text' });
} catch(e) {
$replacement = $('<input>').attr($.extend(args(this), { 'type': 'text' }));
}
$replacement
.removeAttr('name')
.data({
'placeholder-password': true,
'placeholder-id': id
})
.bind('focus.placeholder', clearPlaceholder);
$input
.data({
'placeholder-textinput': $replacement,
'placeholder-id': id
})
.before($replacement);
}
$input = $input.removeAttr('id').hide().prev().attr('id', id).show();
// Note: `$input[0] != input` now!
}
$input.addClass('placeholder');
$input[0].value = $input.attr('placeholder');
} else {
$input.removeClass('placeholder');
}
}
}(this, document, jQuery));

View File

@ -1,4 +1,4 @@
@charset "UTF-8";
@charset "utf-8";
@font-face {
font-family: "mnmliconsRegular";
src: url("/web/static/src/font/mnmliconsv21-webfont.eot") format("eot");
@ -355,6 +355,12 @@
float: right;
margin-left: 8px;
}
.openerp .oe_text_center {
text-align: center;
}
.openerp .oe_text_left {
text-align: left;
}
.openerp .oe_text_right {
text-align: right;
}
@ -1263,7 +1269,7 @@
color: white;
padding: 2px 4px;
margin: 1px 6px 0 0;
border: 1px solid lightgrey;
border: 1px solid lightGray;
text-shadow: 0 1px 1px rgba(0, 0, 0, 0.2);
-moz-border-radius: 4px;
-webkit-border-radius: 4px;
@ -1295,7 +1301,7 @@
transform: scale(1.1);
}
.openerp .oe_secondary_submenu .oe_active {
border-top: 1px solid lightgrey;
border-top: 1px solid lightGray;
border-bottom: 1px solid #dedede;
text-shadow: 0 1px 1px rgba(0, 0, 0, 0.2);
-moz-box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.2), inset 0 -1px 3px rgba(40, 40, 40, 0.2);
@ -1415,7 +1421,13 @@
display: inline-block;
overflow: hidden;
}
.openerp .oe_view_manager {
display: table;
height: inherit;
width: 100%;
}
.openerp .oe_view_manager .oe_view_manager_body {
display: table-row;
height: inherit;
}
.openerp .oe_view_manager .oe_view_manager_view_kanban {
@ -1800,7 +1812,7 @@
}
.openerp .oe_searchview .oe_searchview_drawer {
position: absolute;
z-index: 100;
z-index: 2;
margin-top: 4px;
top: 100%;
right: -1px;
@ -2225,6 +2237,26 @@
.openerp .oe_form .oe_subtotal_footer label.oe_form_label_help {
font-weight: normal;
}
.openerp .oe_form .oe_form_box_info {
background: #ffee99;
border-bottom: 1px solid #ccbb66;
padding: 4px;
}
.openerp .oe_form .oe_form_box_info > p {
margin: auto;
}
.openerp .oe_form .oe_form_box_warning {
background: #bd362f;
border-bottom: 1px solid #990000;
padding: 4px;
}
.openerp .oe_form .oe_form_box_warning * {
color: white;
text-shadow: none;
}
.openerp .oe_form .oe_form_box_warning > p {
margin: auto;
}
.openerp .oe_form .oe_form_button {
margin: 2px;
}
@ -2254,7 +2286,7 @@
}
.openerp .oe_form .oe_form_label_help[for] span, .openerp .oe_form .oe_form_label[for] span {
font-size: 80%;
color: darkgreen;
color: darkGreen;
vertical-align: top;
position: relative;
top: -4px;
@ -2305,6 +2337,13 @@
.openerp .oe_form .oe_form_field_text {
width: 100%;
}
.openerp .oe_form .oe_form_field_text .oe_form_text_content {
text-overflow: ellipsis;
display: inline-block;
white-space: pre-wrap;
overflow-x: hidden;
width: 100%;
}
.openerp .oe_form .oe_form_field_char input,
.openerp .oe_form .oe_form_field_url input,
.openerp .oe_form .oe_form_field_email input,
@ -2326,6 +2365,7 @@
width: 100%;
display: inline-block;
padding: 2px 2px 2px 0px;
vertical-align: top;
}
.openerp .oe_form .oe_form_field input {
margin: 0px;
@ -2371,7 +2411,6 @@
white-space: nowrap;
}
.openerp .oe_form .oe_form_field_boolean {
padding-top: 4px;
width: auto;
}
.openerp .oe_form .oe_datepicker_container {
@ -3093,8 +3132,25 @@
color: #333333;
}
@-moz-document url-prefix() {
.openerp .oe_view_manager .oe_view_manager_switch li {
line-height: 21px;
}
.openerp .oe_searchview .oe_searchview_search {
top: -1px;
}
.openerp .oe_form_field_many2one .oe_m2o_cm_button {
line-height: 18px;
}
.openerp .oe_secondary_submenu {
line-height: 14px;
}
.openerp .oe_webclient .oe_star_on, .openerp .oe_webclient .oe_star_off {
top: 0px;
}
}
.kitten-mode-activated {
background-image: url(http://placekitten.com/g/1365/769);
background-size: cover;
background-attachment: fixed;
}
@ -3147,6 +3203,14 @@ div.ui-widget-overlay {
border-radius: 3px;
}
.openerp .db_option_table td {
padding-bottom: 10px !important;
}
.openerp_ie .placeholder {
color: #afafb6 !important;
font-style: italic !important;
}
.openerp_ie .oe_form_field_boolean input {
background: white;
}
@ -3165,6 +3229,9 @@ div.ui-widget-overlay {
padding-top: 0;
padding-bottom: 0;
}
.openerp_ie .oe_view_manager_view_kanban {
display: table-cell;
}
.openerp_ie .oe_view_manager_buttons button.oe_write_full {
padding-top: 0;
padding-bottom: 0;

View File

@ -370,6 +370,10 @@ $sheet-padding: 16px
.oe_right
float: right
margin-left: 8px
.oe_text_center
text-align: center
.oe_text_left
text-align: left
.oe_text_right
text-align: right
.oe_clear
@ -1137,7 +1141,11 @@ $sheet-padding: 16px
// }}}
// ViewManager common {{{
.oe_view_manager
display: table
height: inherit
width: 100%
.oe_view_manager_body
display: table-row
height: inherit
.oe_view_manager_view_kanban
height: inherit
@ -1432,7 +1440,7 @@ $sheet-padding: 16px
.oe_searchview_drawer
position: absolute
z-index: 100
z-index: 2
// detach drawer from field slightly
margin-top: 4px
top: 100%
@ -1761,7 +1769,21 @@ $sheet-padding: 16px
padding: 2px 11px 2px 0px !important
label.oe_form_label_help
font-weight: normal
.oe_form_box_info
background: #fe9
border-bottom: 1px solid #cb6
padding: 4px
> p
margin: auto
.oe_form_box_warning
background: #bd362f
border-bottom: 1px solid #900
padding: 4px
*
color: white
text-shadow: none
> p
margin: auto
// }}}
// FormView.group {{{
.oe_form
@ -1833,6 +1855,12 @@ $sheet-padding: 16px
.oe_form
.oe_form_field_text
width: 100%
.oe_form_text_content
text-overflow: ellipsis
display: inline-block
white-space: pre-wrap
overflow-x: hidden
width: 100%
.oe_form_field_char input,
.oe_form_field_url input,
.oe_form_field_email input,
@ -1850,6 +1878,7 @@ $sheet-padding: 16px
width: 100%
display: inline-block
padding: 2px 2px 2px 0px
vertical-align: top
input
margin: 0px
input[type="text"], input[type="password"], input[type="file"], select
@ -1879,7 +1908,6 @@ $sheet-padding: 16px
.oe_form_field_datetime
white-space: nowrap
.oe_form_field_boolean
padding-top: 4px
width: auto
.oe_datepicker_container
display: none
@ -2441,9 +2469,22 @@ $sheet-padding: 16px
float: right
color: #333
// }}}
@-moz-document url-prefix()
.openerp
.oe_view_manager .oe_view_manager_switch li
line-height: 21px
.oe_searchview .oe_searchview_search
top: -1px
.oe_form_field_many2one .oe_m2o_cm_button
line-height: 18px
.oe_secondary_submenu
line-height: 14px
.oe_webclient
.oe_star_on, .oe_star_off
top: 0px
// Kitten Mode {{{
.kitten-mode-activated
background-image: url(http://placekitten.com/g/1365/769)
background-size: cover
background-attachment: fixed
>*
@ -2485,8 +2526,16 @@ div.ui-widget-overlay
@include radius(3px)
// }}}
.openerp
.db_option_table
td
padding-bottom: 10px !important
// Internet Explorer 9+ specifics {{{
.openerp_ie
.placeholder
color: $tag-border !important
font-style: italic !important
.oe_form_field_boolean input
background: #fff
.db_option_table .oe_form_field_selection
@ -2502,6 +2551,8 @@ div.ui-widget-overlay
button.oe_highlight
padding-top: 0
padding-bottom: 0
.oe_view_manager_view_kanban
display: table-cell
.oe_view_manager_buttons
button.oe_write_full
padding-top: 0

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 783 B

View File

@ -327,6 +327,34 @@ instance.web.ExceptionHandler = {
*/
instance.web.crash_manager_registry = new instance.web.Registry();
/**
* Handle redirection warnings, which behave more or less like a regular
* warning, with an additional redirection button.
*/
instance.web.RedirectWarningHandler = instance.web.Dialog.extend(instance.web.ExceptionHandler, {
init: function(parent, error) {
this._super(parent);
this.error = error;
},
display: function() {
error = this.error;
error.data.message = error.data.arguments[0];
instance.web.dialog($('<div>' + QWeb.render('CrashManager.warning', {error: error}) + '</div>'), {
title: "OpenERP " + (_.str.capitalize(error.type) || "Warning"),
buttons: [
{text: _t("Ok"), click: function() { $(this).dialog("close"); }},
{text: error.data.arguments[2], click: function() {
window.location.href='#action='+error.data.arguments[1];
$(this).dialog("close");
}}
]
});
this.destroy();
}
});
instance.web.crash_manager_registry.add('openerp.exceptions.RedirectWarning', 'instance.web.RedirectWarningHandler');
instance.web.Loading = instance.web.Widget.extend({
template: _t("Loading"),
init: function(parent) {
@ -502,7 +530,16 @@ instance.web.DatabaseManager = instance.web.Widget.extend({
'login': 'admin',
'password': form_obj['create_admin_pwd'],
'login_successful': function() {
self.do_action("reload");
var action = {
type: "ir.actions.client",
tag: 'reload',
params: {
url_search : {
db: form_obj['db_name'],
},
}
};
self.do_action(action);
},
},
_push_me: false,
@ -613,6 +650,11 @@ instance.web.client_actions.add("database_manager", "instance.web.DatabaseManage
instance.web.Login = instance.web.Widget.extend({
template: "Login",
remember_credentials: true,
events: {
'change input[name=db],select[name=db]': function(ev) {
this.set('database_selector', $(ev.currentTarget).val());
},
},
init: function(parent, action) {
this._super(parent);
@ -624,18 +666,18 @@ instance.web.Login = instance.web.Widget.extend({
if (_.isEmpty(this.params)) {
this.params = $.bbq.getState(true);
}
if (action && action.params && action.params.db) {
this.params.db = action.params.db;
} else if ($.deparam.querystring().db) {
this.params.db = $.deparam.querystring().db;
}
if (this.params.db) {
this.selected_db = this.params.db;
}
if (this.params.login_successful) {
this.on('login_successful', this, this.params.login_successful);
}
if (this.has_local_storage && this.remember_credentials) {
this.selected_db = localStorage.getItem('last_db_login_success');
this.selected_login = localStorage.getItem('last_login_login_success');
if (jQuery.deparam(jQuery.param.querystring()).debug !== undefined) {
this.selected_password = localStorage.getItem('last_password_login_success');
}
}
},
start: function() {
var self = this;
@ -643,10 +685,10 @@ instance.web.Login = instance.web.Widget.extend({
self.$el.find('.oe_login_manage_db').click(function() {
self.do_action("database_manager");
});
self.on('change:database_selector', this, function() {
this.database_selected(this.get('database_selector'));
});
var d = $.when();
if ($.deparam.querystring().db) {
self.params.db = $.deparam.querystring().db;
}
if ($.param.fragment().token) {
self.params.token = $.param.fragment().token;
}
@ -654,16 +696,44 @@ instance.web.Login = instance.web.Widget.extend({
if (self.params.db && self.params.login && self.params.password) {
d = self.do_login(self.params.db, self.params.login, self.params.password);
} else {
if (self.params.db) {
self.on_db_loaded([self.params.db])
} else {
d = self.rpc("/web/database/get_list", {}).done(self.on_db_loaded).fail(self.on_db_failed);
}
d = self.rpc("/web/database/get_list", {})
.done(self.on_db_loaded)
.fail(self.on_db_failed)
.always(function() {
if (self.selected_db && self.has_local_storage && self.remember_credentials) {
self.$("[name=login]").val(localStorage.getItem(self.selected_db + '|last_login') || '');
if (self.session.debug) {
self.$("[name=password]").val(localStorage.getItem(self.selected_db + '|last_password') || '');
}
}
});
}
return d;
},
remember_last_used_database: function(db) {
// This cookie will be used server side in order to avoid db reloading on first visit
var ttl = 24 * 60 * 60 * 365;
document.cookie = [
'last_used_database=' + db,
'path=/',
'max-age=' + ttl,
'expires=' + new Date(new Date().getTime() + ttl * 1000).toGMTString()
].join(';');
},
database_selected: function(db) {
var params = $.deparam.querystring();
params.db = db;
this.remember_last_used_database(db);
this.$('.oe_login_dbpane').empty().text(_t('Loading...'));
this.$('[name=login], [name=password]').prop('readonly', true);
instance.web.redirect('/?' + $.param(params));
},
on_db_loaded: function (result) {
var self = this;
this.db_list = result;
if (!this.selected_db) {
this.selected_db = result[0];
}
this.$("[name=db]").replaceWith(QWeb.render('Login.dblist', { db_list: this.db_list, selected_db: this.selected_db}));
if(this.db_list.length === 0) {
this.do_action("database_manager");
@ -704,17 +774,11 @@ instance.web.Login = instance.web.Widget.extend({
self.hide_error();
self.$(".oe_login_pane").fadeOut("slow");
return this.session.session_authenticate(db, login, password).then(function() {
if (self.has_local_storage) {
if(self.remember_credentials) {
localStorage.setItem('last_db_login_success', db);
localStorage.setItem('last_login_login_success', login);
if (jQuery.deparam(jQuery.param.querystring()).debug !== undefined) {
localStorage.setItem('last_password_login_success', password);
}
} else {
localStorage.setItem('last_db_login_success', '');
localStorage.setItem('last_login_login_success', '');
localStorage.setItem('last_password_login_success', '');
self.remember_last_used_database(db);
if (self.has_local_storage && self.remember_credentials) {
localStorage.setItem(db + '|last_login', login);
if (self.session.debug) {
localStorage.setItem(db + '|last_password', password);
}
}
self.trigger('login_successful');
@ -772,6 +836,9 @@ instance.web.Reload = function(parent, action) {
var sobj = $.deparam(l.search.substr(1));
sobj.ts = new Date().getTime();
if (params.url_search) {
sobj = _.extend(sobj, params.url_search);
}
var search = '?' + $.param(sobj);
var hash = l.hash;
@ -1056,7 +1123,7 @@ instance.web.UserMenu = instance.web.Widget.extend({
if (!self.session.uid)
return;
var func = new instance.web.Model("res.users").get_func("read");
return func(self.session.uid, ["name", "company_id"]).then(function(res) {
return self.alive(func(self.session.uid, ["name", "company_id"])).then(function(res) {
var topbar_name = res.name;
if(instance.session.debug)
topbar_name = _.str.sprintf("%s (%s)", topbar_name, instance.session.db);
@ -1072,6 +1139,9 @@ instance.web.UserMenu = instance.web.Widget.extend({
};
this.update_promise = this.update_promise.then(fct, fct);
},
on_menu_help: function() {
window.open('http://help.openerp.com', '_blank');
},
on_menu_logout: function() {
this.trigger('user_logout');
},
@ -1185,6 +1255,7 @@ instance.web.WebClient = instance.web.Client.extend({
return $.when(this._super()).then(function() {
if (jQuery.param !== undefined && jQuery.deparam(jQuery.param.querystring()).kitten !== undefined) {
$("body").addClass("kitten-mode-activated");
$("body").css("background-image", "url(" + instance.session.origin + "/web/static/src/img/back-enable.jpg" + ")");
if ($.blockUI) {
$.blockUI.defaults.message = '<img src="http://www.amigrave.com/kitten.gif">';
}
@ -1271,7 +1342,7 @@ instance.web.WebClient = instance.web.Client.extend({
},
logo_edit: function(ev) {
var self = this;
new instance.web.Model("res.users").get_func("read")(this.session.uid, ["company_id"]).then(function(res) {
self.alive(new instance.web.Model("res.users").get_func("read")(this.session.uid, ["company_id"])).then(function(res) {
self.rpc("/web/action/load", { action_id: "base.action_res_company_form" }).done(function(result) {
result.res_id = res['company_id'][0];
result.target = "new";
@ -1292,7 +1363,7 @@ instance.web.WebClient = instance.web.Client.extend({
},
check_timezone: function() {
var self = this;
return new instance.web.Model('res.users').call('read', [[this.session.uid], ['tz_offset']]).then(function(result) {
return self.alive(new instance.web.Model('res.users').call('read', [[this.session.uid], ['tz_offset']])).then(function(result) {
var user_offset = result[0]['tz_offset'];
var offset = -(new Date().getTimezoneOffset());
// _.str.sprintf()'s zero front padding is buggy with signed decimals, so doing it manually
@ -1368,8 +1439,9 @@ instance.web.WebClient = instance.web.Client.extend({
},
on_hashchange: function(event) {
var self = this;
var state = event.getState(true);
if (!_.isEqual(this._current_state, state)) {
var stringstate = event.getState(false);
if (!_.isEqual(this._current_state, stringstate)) {
var state = event.getState(true);
if(!state.action && state.menu_id) {
self.menu.has_been_loaded.done(function() {
self.menu.do_reload().done(function() {
@ -1381,13 +1453,13 @@ instance.web.WebClient = instance.web.Client.extend({
this.action_manager.do_load_state(state, !!this._current_state);
}
}
this._current_state = state;
this._current_state = stringstate;
},
do_push_state: function(state) {
this.set_title(state.title);
delete state.title;
var url = '#' + $.param(state);
this._current_state = _.clone(state);
this._current_state = $.deparam($.param(state), false); // stringify all values
$.bbq.pushState(url);
this.trigger('state_pushed', state);
},
@ -1397,9 +1469,10 @@ instance.web.WebClient = instance.web.Client.extend({
.then(function (result) {
return self.action_mutex.exec(function() {
if (options.needaction) {
result.context = new instance.web.CompoundContext(
result.context,
{search_default_message_unread: true});
result.context = new instance.web.CompoundContext(result.context, {
search_default_message_unread: true,
search_disable_custom_filters: true,
});
}
var completed = $.Deferred();
$.when(self.action_manager.do_action(result, {
@ -1466,8 +1539,9 @@ instance.web.EmbeddedClient = instance.web.Client.extend({
});
},
do_action: function(action) {
return this.action_manager.do_action(action);
do_action: function(/*...*/) {
var am = this.action_manager;
return am.do_action.apply(am, arguments);
},
authenticate: function() {

View File

@ -228,6 +228,42 @@ instance.web.ParentedMixin = {
isDestroyed : function() {
return this.__parentedDestroyed;
},
/**
Utility method to only execute asynchronous actions if the current
object has not been destroyed.
@param {$.Deferred} promise The promise representing the asynchronous
action.
@param {bool} [reject=false] If true, the returned promise will be
rejected with no arguments if the current
object is destroyed. If false, the
returned promise will never be resolved
or rejected.
@returns {$.Deferred} A promise that will mirror the given promise if
everything goes fine but will either be rejected
with no arguments or never resolved if the
current object is destroyed.
*/
alive: function(promise, reject) {
var def = $.Deferred();
var self = this;
promise.done(function() {
if (! self.isDestroyed()) {
if (! reject)
def.resolve.apply(def, arguments);
else
def.reject();
}
}).fail(function() {
if (! self.isDestroyed()) {
if (! reject)
def.reject.apply(def, arguments);
else
def.reject();
}
});
return def.promise();
},
/**
* Inform the object it should destroy itself, releasing any
* resource it could have reserved.
@ -495,16 +531,7 @@ instance.web.Controller = instance.web.Class.extend(instance.web.PropertiesMixin
return false;
},
rpc: function(url, data, options) {
var def = $.Deferred();
var self = this;
instance.session.rpc(url, data, options).done(function() {
if (!self.isDestroyed())
def.resolve.apply(def, arguments);
}).fail(function() {
if (!self.isDestroyed())
def.reject.apply(def, arguments);
});
return def.promise();
return this.alive(instance.session.rpc(url, data, options));
}
});
@ -674,10 +701,10 @@ instance.web.Widget = instance.web.Controller.extend({
* Method called after rendering. Mostly used to bind actions, perform asynchronous
* calls, etc...
*
* By convention, the method should return a promise to inform the caller when
* this widget has been initialized.
* By convention, this method should return an object that can be passed to $.when()
* to inform the caller when this widget has been initialized.
*
* @returns {jQuery.Deferred}
* @returns {jQuery.Deferred or any}
*/
start: function() {
return $.when();

View File

@ -89,6 +89,10 @@ instance.web.Session = instance.web.JsonRPC.extend( /** @lends instance.web.Sess
});
},
session_is_valid: function() {
var db = $.deparam.querystring().db;
if (db && this.db !== db) {
return false;
}
return !!this.uid;
},
/**

View File

@ -481,9 +481,12 @@ instance.web.DataSet = instance.web.Class.extend(instance.web.PropertiesMixin,
* Creates a new record in db
*
* @param {Object} data field values to set on the new record
* @param {Object} options Dictionary that can contain the following keys:
* - readonly_fields: Values from readonly fields that were updated by
* on_changes. Only used by the BufferedDataSet to make the o2m work correctly.
* @returns {$.Deferred}
*/
create: function(data) {
create: function(data, options) {
return this._model.call('create', [data], {context: this.get_context()});
},
/**
@ -491,8 +494,10 @@ instance.web.DataSet = instance.web.Class.extend(instance.web.PropertiesMixin,
*
* @param {Number|String} id identifier for the record to alter
* @param {Object} data field values to write into the record
* @param {Function} callback function called with operation result
* @param {Function} error_callback function called in case of write error
* @param {Object} options Dictionary that can contain the following keys:
* - context: The context to use in the server-side call.
* - readonly_fields: Values from readonly fields that were updated by
* on_changes. Only used by the BufferedDataSet to make the o2m work correctly.
* @returns {$.Deferred}
*/
write: function (id, data, options) {
@ -732,10 +737,13 @@ instance.web.BufferedDataSet = instance.web.DataSetStatic.extend({
self.last_default_get = res;
});
},
create: function(data) {
var cached = {id:_.uniqueId(this.virtual_id_prefix), values: data,
defaults: this.last_default_get};
this.to_create.push(_.extend(_.clone(cached), {values: _.clone(cached.values)}));
create: function(data, options) {
var cached = {
id:_.uniqueId(this.virtual_id_prefix),
values: _.extend({}, data, (options || {}).readonly_fields || {}),
defaults: this.last_default_get
};
this.to_create.push(_.extend(_.clone(cached), {values: _.clone(data)}));
this.cache.push(cached);
return $.Deferred().resolve(cached.id).promise();
},
@ -762,7 +770,7 @@ instance.web.BufferedDataSet = instance.web.DataSetStatic.extend({
cached = {id: id, values: {}};
this.cache.push(cached);
}
$.extend(cached.values, record.values);
$.extend(cached.values, _.extend({}, record.values, (options || {}).readonly_fields || {}));
if (dirty)
this.trigger("dataset_changed", id, data, options);
return $.Deferred().resolve(true).promise();
@ -860,8 +868,16 @@ instance.web.BufferedDataSet = instance.web.DataSetStatic.extend({
}
return completion.promise();
},
call_button: function (method, args) {
var id = args[0][0], index;
/**
* Invalidates caching of a record in the dataset to ensure the next read
* of that record will hit the server.
*
* Of use when an action is going to remote-alter a record which will then
* need to be reloaded, e.g. action button.
*
* @param {Object} id record to remove from the BDS's cache
*/
evict_record: function (id) {
for(var i=0, len=this.cache.length; i<len; ++i) {
var record = this.cache[i];
// if record we call the button upon is in the cache
@ -871,8 +887,15 @@ instance.web.BufferedDataSet = instance.web.DataSetStatic.extend({
break;
}
}
},
call_button: function (method, args) {
this.evict_record(args[0][0]);
return this._super(method, args);
},
exec_workflow: function (id, signal) {
this.evict_record(id);
return this._super(id, signal);
},
alter_ids: function(n_ids) {
this._super(n_ids);
this.trigger("dataset_changed", n_ids);
@ -903,9 +926,9 @@ instance.web.ProxyDataSet = instance.web.DataSetSearch.extend({
return this._super.apply(this, arguments);
}
},
create: function(data) {
create: function(data, options) {
if (this.create_function) {
return this.create_function(data, this._super);
return this.create_function(data, options, this._super);
} else {
return this._super.apply(this, arguments);
}
@ -946,7 +969,10 @@ instance.web.CompoundContext = instance.web.Class.extend({
},
get_eval_context: function () {
return this.__eval_context;
}
},
eval: function() {
return instance.web.pyeval.eval('context', this, undefined, {no_user_context: true});
},
});
instance.web.CompoundDomain = instance.web.Class.extend({
@ -969,7 +995,10 @@ instance.web.CompoundDomain = instance.web.Class.extend({
},
get_eval_context: function() {
return this.__eval_context;
}
},
eval: function() {
return instance.web.pyeval.eval('domain', this);
},
});
instance.web.DropMisordered = instance.web.Class.extend({

View File

@ -502,12 +502,15 @@ openerp.web.pyeval = function (instance) {
return py.PY_call(py.PY_getAttr(d, 'strftime'), [args.format]);
});
var args = _.map(('year month day hour minute second microsecond '
+ 'years months weeks days hours minutes secondes microseconds '
+ 'weekday leakdays yearday nlyearday').split(' '), function (arg) {
return [arg, null]
});
args.unshift('*');
var relativedelta = py.type('relativedelta', null, {
__init__: function () {
this.ops = py.PY_parseArgs(arguments,
'* year month day hour minute second microsecond '
+ 'years months weeks days hours minutes secondes microseconds '
+ 'weekday leakdays yearday nlyearday');
this.ops = py.PY_parseArgs(arguments, args);
},
__add__: function (other) {
if (!py.PY_isInstance(other, datetime.date)) {
@ -600,7 +603,7 @@ openerp.web.pyeval = function (instance) {
});
var eval_contexts = function (contexts, evaluation_context) {
evaluation_context = evaluation_context || {};
evaluation_context = _.extend(instance.web.pyeval.context(), evaluation_context || {});
return _(contexts).reduce(function (result_context, ctx) {
// __eval_context evaluations can lead to some of `contexts`'s
// values being null, skip them as well as empty contexts
@ -625,9 +628,10 @@ openerp.web.pyeval = function (instance) {
// siblings
_.extend(evaluation_context, evaluated);
return _.extend(result_context, evaluated);
}, _.extend({}, instance.session.user_context));
}, {});
};
var eval_domains = function (domains, evaluation_context) {
evaluation_context = _.extend(instance.web.pyeval.context(), evaluation_context || {});
var result_domain = [];
_(domains).each(function (domain) {
if (_.isString(domain)) {
@ -654,6 +658,7 @@ openerp.web.pyeval = function (instance) {
return result_domain;
};
var eval_groupbys = function (contexts, evaluation_context) {
evaluation_context = _.extend(instance.web.pyeval.context(), evaluation_context || {});
var result_group = [];
_(contexts).each(function (ctx) {
if (_.isString(ctx)) {
@ -704,14 +709,15 @@ openerp.web.pyeval = function (instance) {
* @param {Array} object domains or contexts to evaluate
* @param {Object} [context] evaluation context
*/
instance.web.pyeval.eval = function (type, object, context) {
instance.web.pyeval.eval = function (type, object, context, options) {
options = options || {};
context = _.extend(instance.web.pyeval.context(), context || {});
context['context'] = py.dict.fromJSON(context);
//noinspection FallthroughInSwitchStatementJS
switch(type) {
case 'context': object = [object];
case 'contexts': return eval_contexts(object, context);
case 'contexts': return eval_contexts((options.no_user_context ? [] : [instance.session.user_context]).concat(object), context);
case 'domain': object = [object];
case 'domains': return eval_domains(object, context);
case 'groupbys': return eval_groupbys(object, context);

View File

@ -324,7 +324,10 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea
e.preventDefault();
break;
}
}
},
'autocompleteopen': function () {
this.$el.autocomplete('widget').css('z-index', 3);
},
},
/**
* @constructs instance.web.SearchView
@ -352,7 +355,7 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea
this.has_defaults = !_.isEmpty(this.defaults);
this.inputs = [];
this.controls = {};
this.controls = [];
this.headless = this.options.hidden && !this.has_defaults;
@ -379,6 +382,7 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea
model: this.dataset._model,
view_id: this.view_id,
view_type: 'search',
context: this.dataset.get_context(),
});
$.when(load_view).then(function (r) {
@ -492,6 +496,7 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea
*/
complete_global_search: function (req, resp) {
$.when.apply(null, _(this.inputs).chain()
.filter(function (input) { return input.visible(); })
.invoke('complete', req.term)
.value()).then(function () {
resp(_(_(arguments).compact()).flatten(true));
@ -580,18 +585,18 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea
*
* @param {Array} items a list of nodes to convert to widgets
* @param {Object} fields a mapping of field names to (ORM) field attributes
* @param {String} [group_name] name of the group to put the new controls in
* @param {Object} [group] group to put the new controls in
*/
make_widgets: function (items, fields, group_name) {
group_name = group_name || null;
if (!(group_name in this.controls)) {
this.controls[group_name] = [];
make_widgets: function (items, fields, group) {
if (!group) {
group = new instance.web.search.Group(
this, 'q', {attrs: {string: _t("Filters")}});
}
var self = this, group = this.controls[group_name];
var self = this;
var filters = [];
_.each(items, function (item) {
if (filters.length && item.tag !== 'filter') {
group.push(new instance.web.search.FilterGroup(filters, this));
group.push(new instance.web.search.FilterGroup(filters, group));
filters = [];
}
@ -599,15 +604,18 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea
case 'separator': case 'newline':
break;
case 'filter':
filters.push(new instance.web.search.Filter(item, this));
filters.push(new instance.web.search.Filter(item, group));
break;
case 'group':
self.make_widgets(item.children, fields, item.attrs.string);
self.make_widgets(item.children, fields,
new instance.web.search.Group(group, 'w', item));
break;
case 'field':
group.push(this.make_field(item, fields[item['attrs'].name]));
var field = this.make_field(
item, fields[item['attrs'].name], group);
group.push(field);
// filters
self.make_widgets(item.children, fields, group_name);
self.make_widgets(item.children, fields, group);
break;
}
}, this);
@ -622,12 +630,13 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea
*
* @param {Object} item fields_view_get node for the field
* @param {Object} field fields_get result for the field
* @param {Object} [parent]
* @returns instance.web.search.Field
*/
make_field: function (item, field) {
make_field: function (item, field, parent) {
var obj = instance.web.search.fields.get_any( [item.attrs.widget, field.type]);
if(obj) {
return new (obj) (item, field, this);
return new (obj) (item, field, parent || this);
} else {
console.group('Unknown field type ' + field.type);
console.error('View node', item);
@ -690,7 +699,7 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea
return filter.user_id && filter.is_default;
});
if (personal_filter) {
this.custom_filters.enable_filter(personal_filter, true);
this.custom_filters.toggle_filter(personal_filter, true);
return;
}
@ -698,7 +707,7 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea
return !filter.user_id && filter.is_default;
});
if (global_filter) {
this.custom_filters.enable_filter(global_filter, true);
this.custom_filters.toggle_filter(global_filter, true);
return;
}
}
@ -862,13 +871,18 @@ instance.web.search.Widget = instance.web.Widget.extend( /** @lends instance.web
* @constructs instance.web.search.Widget
* @extends instance.web.Widget
*
* @param view the ancestor view of this widget
* @param parent parent of this widget
*/
init: function (view) {
this._super(view);
this.view = view;
init: function (parent) {
this._super(parent);
var ancestor = parent;
do {
this.view = ancestor;
} while (!(ancestor instanceof instance.web.SearchView)
&& (ancestor = (ancestor.getParent && ancestor.getParent())));
}
});
instance.web.search.add_expand_listener = function($root) {
$root.find('a.searchview_group_string').click(function (e) {
$root.toggleClass('folded expanded');
@ -877,13 +891,24 @@ instance.web.search.add_expand_listener = function($root) {
});
};
instance.web.search.Group = instance.web.search.Widget.extend({
template: 'SearchView.group',
init: function (view_section, view, fields) {
this._super(view);
this.attrs = view_section.attrs;
this.lines = view.make_widgets(
view_section.children, fields);
}
init: function (parent, icon, node) {
this._super(parent);
var attrs = node.attrs;
this.modifiers = attrs.modifiers =
attrs.modifiers ? JSON.parse(attrs.modifiers) : {};
this.attrs = attrs;
this.icon = icon;
this.name = attrs.string;
this.children = [];
this.view.controls.push(this);
},
push: function (input) {
this.children.push(input);
},
visible: function () {
return !this.modifiers.invisible;
},
});
instance.web.search.Input = instance.web.search.Widget.extend( /** @lends instance.web.search.Input# */{
@ -892,12 +917,12 @@ instance.web.search.Input = instance.web.search.Widget.extend( /** @lends instan
* @constructs instance.web.search.Input
* @extends instance.web.search.Widget
*
* @param view
* @param parent
*/
init: function (view) {
this._super(view);
init: function (parent) {
this._super(parent);
this.load_attrs({});
this.view.inputs.push(this);
this.style = undefined;
},
/**
* Fetch auto-completion values for the widget.
@ -945,15 +970,30 @@ instance.web.search.Input = instance.web.search.Widget.extend( /** @lends instan
"get_domain not implemented for widget " + this.attrs.type);
},
load_attrs: function (attrs) {
if (attrs.modifiers) {
attrs.modifiers = JSON.parse(attrs.modifiers);
attrs.invisible = attrs.modifiers.invisible || false;
if (attrs.invisible) {
this.style = 'display: none;'
attrs.modifiers = attrs.modifiers ? JSON.parse(attrs.modifiers) : {};
this.attrs = attrs;
},
/**
* Returns whether the input is "visible". The default behavior is to
* query the ``modifiers.invisible`` flag on the input's description or
* view node.
*
* @returns {Boolean}
*/
visible: function () {
if (this.attrs.modifiers.invisible) {
return false;
}
var parent = this;
while ((parent = parent.getParent()) &&
( (parent instanceof instance.web.search.Group)
|| (parent instanceof instance.web.search.Input))) {
if (!parent.visible()) {
return false;
}
}
this.attrs = attrs;
}
return true;
},
});
instance.web.search.FilterGroup = instance.web.search.Input.extend(/** @lends instance.web.search.FilterGroup# */{
template: 'SearchView.filters',
@ -967,17 +1007,19 @@ instance.web.search.FilterGroup = instance.web.search.Input.extend(/** @lends in
* @extends instance.web.search.Input
*
* @param {Array<instance.web.search.Filter>} filters elements of the group
* @param {instance.web.SearchView} view view in which the filters are contained
* @param {instance.web.SearchView} parent parent in which the filters are contained
*/
init: function (filters, view) {
init: function (filters, parent) {
// If all filters are group_by and we're not initializing a GroupbyGroup,
// create a GroupbyGroup instead of the current FilterGroup
if (!(this instanceof instance.web.search.GroupbyGroup) &&
_(filters).all(function (f) {
return f.attrs.context && f.attrs.context.group_by; })) {
return new instance.web.search.GroupbyGroup(filters, view);
if (!f.attrs.context) { return false; }
var c = instance.web.pyeval.eval('context', f.attrs.context);
return !_.isEmpty(c.group_by);})) {
return new instance.web.search.GroupbyGroup(filters, parent);
}
this._super(view);
this._super(parent);
this.filters = filters;
this.view.query.on('add remove change reset', this.proxy('search_change'));
},
@ -1096,6 +1138,7 @@ instance.web.search.FilterGroup = instance.web.search.Input.extend(/** @lends in
var self = this;
item = item.toLowerCase();
var facet_values = _(this.filters).chain()
.filter(function (filter) { return filter.visible(); })
.filter(function (filter) {
var at = {
string: filter.attrs.string || '',
@ -1122,8 +1165,8 @@ instance.web.search.FilterGroup = instance.web.search.Input.extend(/** @lends in
instance.web.search.GroupbyGroup = instance.web.search.FilterGroup.extend({
icon: 'w',
completion_label: _lt("Group by: %s"),
init: function (filters, view) {
this._super(filters, view);
init: function (filters, parent) {
this._super(filters, parent);
// Not flanders: facet unicity is handled through the
// (category, field) pair of facet attributes. This is all well and
// good for regular filter groups where a group matches a facet, but for
@ -1131,8 +1174,8 @@ instance.web.search.GroupbyGroup = instance.web.search.FilterGroup.extend({
// view which proxies to the first GroupbyGroup, so it can be used
// for every GroupbyGroup and still provides the various methods needed
// by the search view. Use weirdo name to avoid risks of conflicts
if (!this.getParent()._s_groupby) {
this.getParent()._s_groupby = {
if (!this.view._s_groupby) {
this.view._s_groupby = {
help: "See GroupbyGroup#init",
get_context: this.proxy('get_context'),
get_domain: this.proxy('get_domain'),
@ -1141,14 +1184,14 @@ instance.web.search.GroupbyGroup = instance.web.search.FilterGroup.extend({
}
},
match_facet: function (facet) {
return facet.get('field') === this.getParent()._s_groupby;
return facet.get('field') === this.view._s_groupby;
},
make_facet: function (values) {
return {
category: _t("GroupBy"),
icon: this.icon,
values: values,
field: this.getParent()._s_groupby
field: this.view._s_groupby
};
}
});
@ -1166,10 +1209,10 @@ instance.web.search.Filter = instance.web.search.Input.extend(/** @lends instanc
* @extends instance.web.search.Input
*
* @param node
* @param view
* @param parent
*/
init: function (node, view) {
this._super(view);
init: function (node, parent) {
this._super(parent);
this.load_attrs(node.attrs);
},
facet_for: function () { return $.when(null); },
@ -1185,10 +1228,10 @@ instance.web.search.Field = instance.web.search.Input.extend( /** @lends instanc
*
* @param view_section
* @param field
* @param view
* @param parent
*/
init: function (view_section, field, view) {
this._super(view);
init: function (view_section, field, parent) {
this._super(parent);
this.load_attrs(_.extend({}, field, view_section.attrs));
},
facet_for: function (value) {
@ -1228,7 +1271,7 @@ instance.web.search.Field = instance.web.search.Input.extend( /** @lends instanc
*
* @param {String} name the field's name
* @param {String} operator the field's operator (either attribute-specified or default operator for the field
* @param {Number|String} value parsed value for the field
* @param {Number|String} facet parsed value for the field
* @returns {Array<Array>} domain to include in the resulting search
*/
make_domain: function (name, operator, facet) {
@ -1460,18 +1503,21 @@ instance.web.search.DateTimeField = instance.web.search.DateField.extend(/** @le
});
instance.web.search.ManyToOneField = instance.web.search.CharField.extend({
default_operator: {},
init: function (view_section, field, view) {
this._super(view_section, field, view);
init: function (view_section, field, parent) {
this._super(view_section, field, parent);
this.model = new instance.web.Model(this.attrs.relation);
},
complete: function (needle) {
var self = this;
// TODO: context
// FIXME: "concurrent" searches (multiple requests, mis-ordered responses)
var context = instance.web.pyeval.eval(
'contexts', [this.view.dataset.get_context()]);
return this.model.call('name_search', [], {
name: needle,
args: instance.web.pyeval.eval(
'domains', this.attrs.domain ? [this.attrs.domain] : [], context),
limit: 8,
context: {}
context: context
}).then(function (results) {
if (_.isEmpty(results)) { return null; }
return [{label: self.attrs.string}].concat(
@ -1541,6 +1587,9 @@ instance.web.search.CustomFilters = instance.web.search.Input.extend({
})
.on('reset', this.proxy('clear_selection'));
this.$el.on('submit', 'form', this.proxy('save_current'));
this.$el.on('click', 'input[type=checkbox]', function() {
$(this).siblings('input[type=checkbox]').prop('checked', false);
});
this.$el.on('click', 'h4', function () {
self.$el.toggleClass('oe_opened');
});
@ -1591,6 +1640,7 @@ instance.web.search.CustomFilters = instance.web.search.Input.extend({
get_groupby: function () { return [filter.context]; },
get_domain: function () { return filter.domain; }
},
_id: filter['id'],
is_custom_filter: true,
values: [{label: filter.name, value: null}]
};
@ -1632,10 +1682,18 @@ instance.web.search.CustomFilters = instance.web.search.Input.extend({
}
$filter.unbind('click').click(function () {
self.enable_filter(filter);
self.toggle_filter(filter);
});
},
enable_filter: function (filter, preventSearch) {
toggle_filter: function (filter, preventSearch) {
var current = this.view.query.find(function (facet) {
return facet.get('_id') === filter.id;
});
if (current) {
this.view.query.remove(current);
this.$filters[this.key_for(filter)].removeClass('oe_selected');
return;
}
this.view.query.reset([this.facet_for(filter)], {
preventSearch: preventSearch || false});
this.$filters[this.key_for(filter)].addClass('oe_selected');
@ -1658,6 +1716,13 @@ instance.web.search.CustomFilters = instance.web.search.Input.extend({
if (!_.isEmpty(results.group_by)) {
results.context.group_by = results.group_by;
}
// Don't save user_context keys in the custom filter, otherwise end
// up with e.g. wrong uid or lang stored *and used in subsequent
// reqs*
var ctx = results.context;
_(_.keys(instance.session.user_context)).each(function (key) {
delete ctx[key];
});
var filter = {
name: $name.val(),
user_id: private_filter ? instance.session.uid : false,
@ -1687,22 +1752,28 @@ instance.web.search.Filters = instance.web.search.Input.extend({
var running_count = 0;
// get total filters count
var is_group = function (i) { return i instanceof instance.web.search.FilterGroup; };
var filters_count = _(this.view.controls).chain()
var visible_filters = _(this.view.controls).chain().reject(function (group) {
return _(_(group.children).filter(is_group)).isEmpty()
|| group.modifiers.invisible;
});
var filters_count = visible_filters
.pluck('children')
.flatten()
.filter(is_group)
.map(function (i) { return i.filters.length; })
.sum()
.value();
var col1 = [], col2 = _(this.view.controls).map(function (inputs, group) {
var filters = _(inputs).filter(is_group);
return {
name: group === 'null' ? "<span class='oe_i'>q</span> " + _t("Filters") : "<span class='oe_i'>w</span> " + group,
filters: filters,
length: _(filters).chain().map(function (i) {
return i.filters.length; }).sum().value()
};
});
var col1 = [], col2 = visible_filters.map(function (group) {
var filters = _(group.children).filter(is_group);
return {
name: _.str.sprintf("<span class='oe_i'>%s</span> %s",
group.icon, group.name),
filters: filters,
length: _(filters).chain().map(function (i) {
return i.filters.length; }).sum().value()
};
}).value();
while (col2.length) {
// col1 + group should be smaller than col2 + group
@ -1800,6 +1871,7 @@ instance.web.search.ExtendedSearchProposition = instance.web.Widget.extend(/** @
template: 'SearchView.extended_search.proposition',
events: {
'change .searchview_extended_prop_field': 'changed',
'change .searchview_extended_prop_op': 'operator_changed',
'click .searchview_extended_delete_prop': function (e) {
e.stopPropagation();
this.getParent().remove_proposition(this);
@ -1831,6 +1903,17 @@ instance.web.search.ExtendedSearchProposition = instance.web.Widget.extend(/** @
this.select_field(_.detect(this.fields, function(x) {return x.name == nval;}));
}
},
operator_changed: function (e) {
var $value = this.$('.searchview_extended_prop_value');
switch ($(e.target).val()) {
case '∃':
case '∄':
$value.hide();
break;
default:
$value.show();
}
},
/**
* Selects the provided field object
*
@ -1859,7 +1942,7 @@ instance.web.search.ExtendedSearchProposition = instance.web.Widget.extend(/** @
.text(String(operator.text))
.appendTo(self.$('.searchview_extended_prop_op'));
});
var $value_loc = this.$('.searchview_extended_prop_value').empty();
var $value_loc = this.$('.searchview_extended_prop_value').show().empty();
this.value.appendTo($value_loc);
},
@ -1867,19 +1950,12 @@ instance.web.search.ExtendedSearchProposition = instance.web.Widget.extend(/** @
if ( this.attrs.selected == null)
return null;
var field = this.attrs.selected;
var op = this.$('.searchview_extended_prop_op')[0];
var operator = op.options[op.selectedIndex];
var op_select = this.$('.searchview_extended_prop_op')[0];
var operator = op_select.options[op_select.selectedIndex];
return {
label: _.str.sprintf(_t('%(field)s %(operator)s "%(value)s"'), {
field: field.string,
// According to spec, HTMLOptionElement#label should return
// HTMLOptionElement#text when not defined/empty, but it does
// not in older Webkit (between Safari 5.1.5 and Chrome 17) and
// Gecko (pre Firefox 7) browsers, so we need a manual fallback
// for those
operator: operator.label || operator.text,
value: this.value}),
value: [field.name, operator.value, this.value.get_value()]
label: this.value.get_label(field, operator),
value: this.value.get_domain(field, operator),
};
}
});
@ -1889,6 +1965,37 @@ instance.web.search.ExtendedSearchProposition.Field = instance.web.Widget.extend
this._super(parent);
this.field = field;
},
get_label: function (field, operator) {
var format;
switch (operator.value) {
case '∃': case '∄': format = _t('%(field)s %(operator)s'); break;
default: format = _t('%(field)s %(operator)s "%(value)s"'); break;
}
return this.format_label(format, field, operator);
},
format_label: function (format, field, operator) {
return _.str.sprintf(format, {
field: field.string,
// According to spec, HTMLOptionElement#label should return
// HTMLOptionElement#text when not defined/empty, but it does
// not in older Webkit (between Safari 5.1.5 and Chrome 17) and
// Gecko (pre Firefox 7) browsers, so we need a manual fallback
// for those
operator: operator.label || operator.text,
value: this
});
},
get_domain: function (field, operator) {
switch (operator.value) {
case '∃': return this.make_domain(field.name, '!=', false);
case '∄': return this.make_domain(field.name, '=', false);
default: return this.make_domain(
field.name, operator.value, this.get_value());
}
},
make_domain: function (field, operator, value) {
return [field, operator, value];
},
/**
* Returns a human-readable version of the value, in case the "logical"
* and the "semantic" values of a field differ (as for selection fields,
@ -1908,7 +2015,9 @@ instance.web.search.ExtendedSearchProposition.Char = instance.web.search.Extende
{value: "ilike", text: _lt("contains")},
{value: "not ilike", text: _lt("doesn't contain")},
{value: "=", text: _lt("is equal to")},
{value: "!=", text: _lt("is not equal to")}
{value: "!=", text: _lt("is not equal to")},
{value: "∃", text: _lt("is set")},
{value: "∄", text: _lt("is not set")}
],
get_value: function() {
return this.$el.val();
@ -1922,7 +2031,9 @@ instance.web.search.ExtendedSearchProposition.DateTime = instance.web.search.Ext
{value: ">", text: _lt("greater than")},
{value: "<", text: _lt("less than")},
{value: ">=", text: _lt("greater or equal than")},
{value: "<=", text: _lt("less or equal than")}
{value: "<=", text: _lt("less or equal than")},
{value: "∃", text: _lt("is set")},
{value: "∄", text: _lt("is not set")}
],
/**
* Date widgets live in view_form which is not yet loaded when this is
@ -1956,7 +2067,9 @@ instance.web.search.ExtendedSearchProposition.Integer = instance.web.search.Exte
{value: ">", text: _lt("greater than")},
{value: "<", text: _lt("less than")},
{value: ">=", text: _lt("greater or equal than")},
{value: "<=", text: _lt("less or equal than")}
{value: "<=", text: _lt("less or equal than")},
{value: "∃", text: _lt("is set")},
{value: "∄", text: _lt("is not set")}
],
toString: function () {
return this.$el.val();
@ -1981,7 +2094,9 @@ instance.web.search.ExtendedSearchProposition.Float = instance.web.search.Extend
{value: ">", text: _lt("greater than")},
{value: "<", text: _lt("less than")},
{value: ">=", text: _lt("greater or equal than")},
{value: "<=", text: _lt("less or equal than")}
{value: "<=", text: _lt("less or equal than")},
{value: "∃", text: _lt("is set")},
{value: "∄", text: _lt("is not set")}
],
toString: function () {
return this.$el.val();
@ -1999,7 +2114,9 @@ instance.web.search.ExtendedSearchProposition.Selection = instance.web.search.Ex
template: 'SearchView.extended_search.proposition.selection',
operators: [
{value: "=", text: _lt("is")},
{value: "!=", text: _lt("is not")}
{value: "!=", text: _lt("is not")},
{value: "∃", text: _lt("is set")},
{value: "∄", text: _lt("is not set")}
],
toString: function () {
var select = this.$el[0];
@ -2016,7 +2133,10 @@ instance.web.search.ExtendedSearchProposition.Boolean = instance.web.search.Exte
{value: "=", text: _lt("is true")},
{value: "!=", text: _lt("is false")}
],
toString: function () { return ''; },
get_label: function (field, operator) {
return this.format_label(
_t('%(field)s %(operator)s'), field, operator);
},
get_value: function() {
return true;
}

View File

@ -247,13 +247,11 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM
do_load_state: function(state, warm) {
if (state.id && this.datarecord.id != state.id) {
if (!this.dataset.get_id_index(state.id)) {
if (this.dataset.get_id_index(state.id) === null) {
this.dataset.ids.push(state.id);
}
this.dataset.select_id(state.id);
if (warm) {
this.do_show();
}
this.do_show({ reload: warm });
}
},
/**
@ -511,9 +509,13 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM
var on_change = widget.node.attrs.on_change;
if (on_change) {
var change_spec = self.parse_on_change(on_change, widget);
var id = [self.datarecord.id == null ? [] : [self.datarecord.id]];
def = new instance.web.Model(self.dataset.model).call(
change_spec.method, id.concat(change_spec.args));
var ids = [];
if (self.datarecord.id && !instance.web.BufferedDataSet.virtual_id_regex.test(self.datarecord.id)) {
// In case of a o2m virtual id, we should pass an empty ids list
ids.push(self.datarecord.id);
}
def = self.alive(new instance.web.Model(self.dataset.model).call(
change_spec.method, [ids].concat(change_spec.args)));
} else {
def = $.when({});
}
@ -531,9 +533,9 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM
var condition = fieldname + '=' + value_;
if (value_) {
return new instance.web.Model('ir.values').call(
return self.alive(new instance.web.Model('ir.values').call(
'get_defaults', [self.model, condition]
).then(function (results) {
)).then(function (results) {
if (!results.length) {
return response;
}
@ -808,7 +810,8 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM
try {
var form_invalid = false,
values = {},
first_invalid_field = null;
first_invalid_field = null,
readonly_values = {};
for (var f in self.fields) {
if (!self.fields.hasOwnProperty(f)) { continue; }
f = self.fields[f];
@ -817,11 +820,15 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM
if (!first_invalid_field) {
first_invalid_field = f;
}
} else if (f.name !== 'id' && !f.get("readonly") && (!self.datarecord.id || f._dirty_flag)) {
} else if (f.name !== 'id' && (!self.datarecord.id || f._dirty_flag)) {
// Special case 'id' field, do not save this field
// on 'create' : save all non readonly fields
// on 'edit' : save non readonly modified fields
values[f.name] = f.get_value();
if (!f.get("readonly")) {
values[f.name] = f.get_value();
} else {
readonly_values[f.name] = f.get_value();
}
}
}
if (form_invalid) {
@ -834,7 +841,7 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM
var save_deferral;
if (!self.datarecord.id) {
// Creation save
save_deferral = self.dataset.create(values).then(function(r) {
save_deferral = self.dataset.create(values, {readonly_fields: readonly_values}).then(function(r) {
return self.record_created(r, prepend_on_create);
}, null);
} else if (_.isEmpty(values)) {
@ -842,7 +849,7 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM
save_deferral = $.Deferred().resolve({}).promise();
} else {
// Write save
save_deferral = self.dataset.write(self.datarecord.id, values, {}).then(function(r) {
save_deferral = self.dataset.write(self.datarecord.id, values, {readonly_fields: readonly_values}).then(function(r) {
return self.record_saved(r);
}, null);
}
@ -1191,6 +1198,10 @@ instance.web.form.FormRenderingEngine = instance.web.form.FormRenderingEngineInt
$('button', doc).each(function() {
$(this).attr('data-button-type', $(this).attr('type')).attr('type', 'button');
});
// IE's html parser is also a css parser. How convenient...
$('board', doc).each(function() {
$(this).attr('layout', $(this).attr('style'));
});
return $('<div class="oe_form"/>').append(instance.web.xml_to_str(doc));
},
render_to: function($target) {
@ -1881,10 +1892,11 @@ instance.web.form.WidgetButton = instance.web.form.FormWidget.extend({
if (this.node.attrs.icon && (! /\//.test(this.node.attrs.icon))) {
this.node.attrs.icon = '/web/static/src/img/icons/' + this.node.attrs.icon + '.png';
}
this.view.on('view_content_has_changed', this, this.check_disable);
},
start: function() {
this._super.apply(this, arguments);
this.view.on('view_content_has_changed', this, this.check_disable);
this.check_disable();
this.$el.click(this.on_click);
if (this.node.attrs.help || instance.session.debug) {
this.do_attach_tooltip();
@ -1910,18 +1922,20 @@ instance.web.form.WidgetButton = instance.web.form.FormWidget.extend({
modal: true,
buttons: [
{text: _t("Cancel"), click: function() {
def.resolve();
$(this).dialog("close");
}
},
{text: _t("Ok"), click: function() {
self.on_confirmed().done(function() {
def.resolve();
var self2 = this;
self.on_confirmed().always(function() {
$(self2).dialog("close");
});
$(this).dialog("close");
}
}
]
],
beforeClose: function() {
def.resolve();
},
});
return def.promise();
} else {
@ -2212,6 +2226,18 @@ instance.web.form.ReinitializeFieldMixin = _.extend({}, instance.web.form.Reini
},
});
/**
Some hack to make placeholders work in ie9.
*/
if ($.browser.msie && $.browser.version === "9.0") {
document.addEventListener("DOMNodeInserted",function(event){
var nodename = event.target.nodeName.toLowerCase();
if ( nodename === "input" || nodename == "textarea" ) {
$(event.target).placeholder();
}
});
}
instance.web.form.FieldChar = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
template: 'FieldChar',
widget_class: 'oe_form_field_char',
@ -2327,11 +2353,12 @@ instance.web.form.FieldUrl = instance.web.form.FieldChar.extend({
this._super();
} else {
var tmp = this.get('value');
var s = /(\w+):(.+)/.exec(tmp);
var s = /(\w+):(.+)|^\.{0,2}\//.exec(tmp);
if (!s) {
tmp = "http://" + this.get('value');
}
this.$el.find('a').attr('href', tmp).text(this.get('value') ? tmp : '');
var text = this.get('value') ? this.node.attrs.text || tmp : '';
this.$el.find('a').attr('href', tmp).text(text);
}
},
on_button_clicked: function() {
@ -2395,6 +2422,11 @@ instance.web.DateTimeWidget = instance.web.Widget.extend({
showButtonPanel: true,
firstDay: Date.CultureInfo.firstDayOfWeek
});
// Some clicks in the datepicker dialog are not stopped by the
// datepicker and "bubble through", unexpectedly triggering the bus's
// click event. Prevent that.
this.picker('widget').click(function (e) { e.stopPropagation(); });
this.$el.find('img.oe_datepicker_trigger').click(function() {
if (self.get("effective_readonly") || self.picker('widget').is(':visible')) {
self.$input.focus();
@ -2534,42 +2566,45 @@ instance.web.form.FieldText = instance.web.form.AbstractField.extend(instance.we
},
'change textarea': 'store_dom_value',
},
init: function (field_manager, node) {
this._super(field_manager, node);
},
initialize_content: function() {
var self = this;
this.$textarea = this.$el.find('textarea');
this.auto_sized = false;
this.default_height = this.$textarea.css('height');
if (this.get("effective_readonly")) {
this.$textarea.attr('disabled', 'disabled');
if (! this.get("effective_readonly")) {
this.$textarea = this.$el.find('textarea');
this.auto_sized = false;
this.default_height = this.$textarea.css('height');
if (this.get("effective_readonly")) {
this.$textarea.attr('disabled', 'disabled');
}
this.setupFocus(this.$textarea);
} else {
this.$textarea = undefined;
}
this.setupFocus(this.$textarea);
},
commit_value: function () {
this.store_dom_value();
if (! this.get("effective_readonly") && this.$textarea) {
this.store_dom_value();
}
return this._super();
},
store_dom_value: function () {
if (!this.get('effective_readonly') && this.$('textarea').length) {
this.internal_set_value(
instance.web.parse_value(
this.$textarea.val(),
this));
}
this.internal_set_value(instance.web.parse_value(this.$textarea.val(), this));
},
render_value: function() {
var show_value = instance.web.format_value(this.get('value'), this, '');
if (show_value === '') {
this.$textarea.css('height', parseInt(this.default_height)+"px");
}
this.$textarea.val(show_value);
if (! this.auto_sized) {
this.auto_sized = true;
this.$textarea.autosize();
if (! this.get("effective_readonly")) {
var show_value = instance.web.format_value(this.get('value'), this, '');
if (show_value === '') {
this.$textarea.css('height', parseInt(this.default_height)+"px");
}
this.$textarea.val(show_value);
if (! this.auto_sized) {
this.auto_sized = true;
this.$textarea.autosize();
} else {
this.$textarea.trigger("autosize");
}
} else {
this.$textarea.trigger("autosize");
var txt = this.get("value") || '';
this.$(".oe_form_text_content").text(txt);
}
},
is_syntax_valid: function() {
@ -2587,14 +2622,18 @@ instance.web.form.FieldText = instance.web.form.AbstractField.extend(instance.we
return this.get('value') === '' || this._super();
},
focus: function($el) {
this.$textarea[0].focus();
if (!this.get("effective_readonly") && this.$textarea) {
this.$textarea[0].focus();
}
},
set_dimensions: function (height, width) {
this._super(height, width);
this.$textarea.css({
width: width,
minHeight: height
});
if (!this.get("effective_readonly") && this.$textarea) {
this.$textarea.css({
width: width,
minHeight: height
});
}
},
});
@ -2625,7 +2664,7 @@ instance.web.form.FieldTextHtml = instance.web.form.AbstractField.extend(instanc
"| removeformat | bullets numbering | outdent " +
"indent | link unlink | source",
bodyStyle: // style to assign to document body contained within the editor
"margin:4px; color:#4c4c4c; font-size:13px; font-family:\"Lucida Grande\",Helvetica,Verdana,Arial,sans-serif; cursor:text"
"margin:4px; color:#4c4c4c; font-size:13px; font-family:'Lucida Grande',Helvetica,Verdana,Arial,sans-serif; cursor:text"
});
this.$cleditor = this.$textarea.cleditor()[0];
this.$cleditor.change(function() {
@ -2957,7 +2996,7 @@ instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(instanc
case $.ui.keyCode.DOWN:
e.stopPropagation();
}
}
},
},
init: function(field_manager, node) {
this._super(field_manager, node);
@ -2988,6 +3027,26 @@ instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(instanc
if (!this.get("effective_readonly"))
this.render_editable();
},
destroy_content: function () {
if (this.$drop_down) {
this.$drop_down.off('click');
delete this.$drop_down;
}
if (this.$input) {
this.$input.closest(".ui-dialog .ui-dialog-content").off('scroll');
this.$input.off('keyup blur autocompleteclose autocompleteopen ' +
'focus focusout change keydown');
delete this.$input;
}
if (this.$follow_button) {
this.$follow_button.off('blur focus click');
delete this.$follow_button;
}
},
destroy: function () {
this.destroy_content();
return this._super();
},
init_error_displayer: function() {
// nothing
},
@ -3047,9 +3106,9 @@ instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(instanc
this.$input.keydown(input_changed);
this.$input.change(input_changed);
this.$drop_down.click(function() {
self.$input.focus();
if (self.$input.autocomplete("widget").is(":visible")) {
self.$input.autocomplete("close");
self.$input.focus();
self.$input.autocomplete("close");
} else {
if (self.get("value") && ! self.floating) {
self.$input.autocomplete("search", "");
@ -3058,6 +3117,15 @@ instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(instanc
}
}
});
// Autocomplete close on dialog content scroll
var close_autocomplete = _.debounce(function() {
if (self.$input.autocomplete("widget").is(":visible")) {
self.$input.autocomplete("close");
}
}, 50);
this.$input.closest(".ui-dialog .ui-dialog-content").on('scroll', this, close_autocomplete);
self.ed_def = $.Deferred();
self.uned_def = $.Deferred();
var ed_delay = 200;
@ -3209,7 +3277,8 @@ instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(instanc
res_model: self.field.relation,
res_id: self.get("value"),
views: [[false, 'form']],
target: 'current'
target: 'current',
context: self.build_context().eval(),
});
return false;
});
@ -3240,7 +3309,7 @@ instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(instanc
},
focus: function () {
if (!this.get('effective_readonly')) {
this.$input[0].focus();
this.$input && this.$input[0].focus();
}
},
_quick_create: function() {
@ -3659,8 +3728,8 @@ instance.web.form.One2ManyViewManager = instance.web.ViewManager.extend({
var pop = new instance.web.form.FormOpenPopup(this);
pop.show_element(self.o2m.field.relation, id, self.o2m.build_context(), {
title: _t("Open: ") + self.o2m.string,
create_function: function(data) {
return self.o2m.dataset.create(data).done(function(r) {
create_function: function(data, options) {
return self.o2m.dataset.create(data, options).done(function(r) {
self.o2m.dataset.set_ids(self.o2m.dataset.ids.concat([r]));
self.o2m.dataset.trigger("dataset_changed", r);
});
@ -3719,8 +3788,12 @@ instance.web.form.One2ManyListView = instance.web.ListView.extend({
this.o2m.trigger_on_change();
},
is_valid: function () {
var form = this.editor.form;
var editor = this.editor;
var form = editor.form;
// If no edition is pending, the listview can not be invalid (?)
if (!editor.record) {
return true
}
// If the form has not been modified, the view can only be valid
// NB: is_dirty will also be set on defaults/onchanges/whatever?
// oe_form_dirty seems to only be set on actual user actions
@ -3750,11 +3823,11 @@ instance.web.form.One2ManyListView = instance.web.ListView.extend({
title: _t("Create: ") + self.o2m.string,
initial_view: "form",
alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined,
create_function: function(data, callback, error_callback) {
return self.o2m.dataset.create(data).done(function(r) {
create_function: function(data, options) {
return self.o2m.dataset.create(data, options).done(function(r) {
self.o2m.dataset.set_ids(self.o2m.dataset.ids.concat([r]));
self.o2m.dataset.trigger("dataset_changed", r);
}).done(callback).fail(error_callback);
});
},
read_function: function() {
return self.o2m.dataset.read_ids.apply(self.o2m.dataset, arguments);
@ -3967,6 +4040,7 @@ instance.web.form.FieldMany2ManyTags = instance.web.form.AbstractField.extend(in
if (this.get("effective_readonly"))
return;
var self = this;
var ignore_blur = false;
self.$text = this.$("textarea");
self.$text.textext({
plugins : 'tags arrow autocomplete',
@ -3985,6 +4059,7 @@ instance.web.form.FieldMany2ManyTags = instance.web.form.AbstractField.extend(in
if (data.id) {
self.add_id(data.id);
} else {
ignore_blur = true;
data.action();
}
},
@ -4035,10 +4110,13 @@ instance.web.form.FieldMany2ManyTags = instance.web.form.AbstractField.extend(in
self.$text
.focusin(function () {
self.trigger('focused');
ignore_blur = false;
})
.focusout(function() {
self.$text.trigger("setInputData", "");
self.trigger('blurred');
if (!ignore_blur) {
self.trigger('blurred');
}
}).keydown(function(e) {
if (e.which === $.ui.keyCode.TAB && self._drop_shown) {
self.$text.textext()[0].autocomplete().selectFromDropdown();
@ -4094,6 +4172,13 @@ instance.web.form.FieldMany2ManyTags = instance.web.form.AbstractField.extend(in
focus: function () {
this.$text[0].focus();
},
set_dimensions: function (height, width) {
this._super(height, width);
this.$("textarea").css({
width: width,
minHeight: height
});
},
});
/**
@ -4238,6 +4323,7 @@ instance.web.form.Many2ManyListView = instance.web.ListView.extend(/** @lends in
});
}
},
is_action_enabled: function () { return true; },
});
instance.web.form.FieldMany2ManyKanban = instance.web.form.AbstractField.extend(instance.web.form.CompletionFieldMixin, {
@ -4450,6 +4536,7 @@ instance.web.form.AbstractFormPopup = instance.web.Widget.extend({
* options:
* -readonly: only applicable when not in creation mode, default to false
* - alternative_form_view
* - view_id
* - write_function
* - read_function
* - create_function
@ -4471,9 +4558,9 @@ instance.web.form.AbstractFormPopup = instance.web.Widget.extend({
this.created_elements = [];
this.dataset = new instance.web.ProxyDataSet(this, this.model, this.context);
this.dataset.read_function = this.options.read_function;
this.dataset.create_function = function(data, sup) {
this.dataset.create_function = function(data, options, sup) {
var fct = self.options.create_function || sup;
return fct.call(this, data).done(function(r) {
return fct.call(this, data, options).done(function(r) {
self.trigger('create_completed saved', r);
self.created_elements.push(r);
});
@ -4516,7 +4603,7 @@ instance.web.form.AbstractFormPopup = instance.web.Widget.extend({
_.extend(options, {
$buttons: this.$buttonpane,
});
this.view_form = new instance.web.FormView(this, this.dataset, false, options);
this.view_form = new instance.web.FormView(this, this.dataset, this.options.view_id || false, options);
if (this.options.alternative_form_view) {
this.view_form.set_embedded_view(this.options.alternative_form_view);
}
@ -4545,6 +4632,7 @@ instance.web.form.AbstractFormPopup = instance.web.Widget.extend({
});
var $cbutton = self.$buttonpane.find(".oe_abstractformpopup-form-close");
$cbutton.click(function() {
self.view_form.trigger('on_button_cancel');
self.check_exit();
});
self.view_form.do_show();
@ -5014,7 +5102,7 @@ instance.web.form.FieldBinaryImage = instance.web.form.FieldBinary.extend({
});
/**
* Widget for (one2many field) to upload one or more file in same time and display in list.
* Widget for (many2many field) to upload one or more file in same time and display in list.
* The user can delete his files.
* Options on attribute ; "blockui" {Boolean} block the UI or not
* during the file is uploading
@ -5028,6 +5116,8 @@ instance.web.form.FieldMany2ManyBinaryMultiFiles = instance.web.form.AbstractFie
if(this.field.type != "many2many" || this.field.relation != 'ir.attachment') {
throw _.str.sprintf(_t("The type of the field '%s' must be a many2many field with a relation to 'ir.attachment' model."), this.field.string);
}
this.data = {};
this.set_value([]);
this.ds_file = new instance.web.DataSetSearch(this, 'ir.attachment');
this.fileupload_id = _.uniqueId('oe_fileupload_temp');
$(window).on(this.fileupload_id, _.bind(this.on_file_loaded, this));
@ -5037,73 +5127,39 @@ instance.web.form.FieldMany2ManyBinaryMultiFiles = instance.web.form.AbstractFie
this.$el.on('change', 'input.oe_form_binary_file', this.on_file_change );
},
set_value: function(value_) {
var value_ = value_ || [];
var self = this;
var ids = [];
_.each(value_, function(command) {
if (isNaN(command) && command.id == undefined) {
switch (command[0]) {
case commands.CREATE:
ids = ids.concat(command[2]);
return;
case commands.REPLACE_WITH:
ids = ids.concat(command[2]);
return;
case commands.UPDATE:
ids = ids.concat(command[2]);
return;
case commands.LINK_TO:
ids = ids.concat(command[1]);
return;
case commands.DELETE:
ids = _.filter(ids, function (id) { return id != command[1];});
return;
case commands.DELETE_ALL:
ids = [];
return;
}
} else {
ids.push(command);
}
});
this._super( ids );
value_ = value_ || [];
if (value_.length >= 1 && value_[0] instanceof Array) {
value_ = value_[0][2];
}
this._super(value_);
},
get_value: function() {
return _.map(this.get('value'), function (value) { return commands.link_to( isNaN(value) ? value.id : value ); });
var tmp = [commands.replace_with(this.get("value"))];
return tmp;
},
get_file_url: function (attachment) {
return this.session.url('/web/binary/saveas', {model: 'ir.attachment', field: 'datas', filename_field: 'datas_fname', id: attachment['id']});
},
read_name_values : function () {
var self = this;
// select the list of id for a get_name
var values = [];
_.each(this.get('value'), function (val) {
if (typeof val != 'object') {
values.push(val);
}
});
// don't reset know values
var _value = _.filter(this.get('value'), function (id) { return typeof self.data[id] == 'undefined'; } );
// send request for get_name
if (values.length) {
return this.ds_file.call('read', [values, ['id', 'name', 'datas_fname']]).done(function (datas) {
if (_value.length) {
return this.ds_file.call('read', [_value, ['id', 'name', 'datas_fname']]).done(function (datas) {
_.each(datas, function (data) {
data.no_unlink = true;
data.url = self.session.url('/web/binary/saveas', {model: 'ir.attachment', field: 'datas', filename_field: 'datas_fname', id: data.id});
_.each(self.get('value'), function (val, key) {
if(val == data.id) {
self.get('value')[key] = data;
}
});
self.data[data.id] = data;
});
});
} else {
return $.when(this.get('value'));
return $.when();
}
},
render_value: function () {
var self = this;
this.read_name_values().then(function (datas) {
this.read_name_values().then(function () {
var render = $(instance.web.qweb.render('FieldBinaryFileUploader.files', {'widget': self}));
render.on('click', '.oe_delete', _.bind(self.on_file_delete, self));
@ -5121,45 +5177,36 @@ instance.web.form.FieldMany2ManyBinaryMultiFiles = instance.web.form.AbstractFie
var self = this;
var $target = $(event.target);
if ($target.val() !== '') {
var filename = $target.val().replace(/.*[\\\/]/,'');
// if the files is currently uploded, don't send again
if( !isNaN(_.find(this.get('value'), function (file) { return (file.filename || file.name) == filename && file.upload; } )) ) {
// don't uplode more of one file in same time
if (self.data[0] && self.data[0].upload ) {
return false;
}
for (var id in this.get('value')) {
// if the files exits, delete the file before upload (if it's a new file)
if (self.data[id] && (self.data[id].filename || self.data[id].name) == filename && !self.data[id].no_unlink ) {
self.ds_file.unlink([id]);
}
}
// block UI or not
if(this.node.attrs.blockui>0) {
instance.web.blockUI();
}
// if the files exits for this answer, delete the file before upload
var files = _.filter(this.get('value'), function (file) {
if((file.filename || file.name) == filename) {
self.ds_file.unlink([file.id]);
return false;
} else {
return true;
}
});
// TODO : unactivate send on wizard and form
// submit file
this.$('form.oe_form_binary_form').submit();
this.$(".oe_fileupload").hide();
// add file on result
files.push({
// add file on data result
this.data[0] = {
'id': 0,
'name': filename,
'filename': filename,
'url': '',
'upload': true
});
this.set({'value': files});
};
}
},
on_file_loaded: function (event, result) {
@ -5170,39 +5217,39 @@ instance.web.form.FieldMany2ManyBinaryMultiFiles = instance.web.form.AbstractFie
instance.web.unblockUI();
}
// TODO : activate send on wizard and form
if (result.error || !result.id ) {
this.do_warn( _t('Uploading error'), result.error);
files = _.filter(files, function (val) { return !val.upload; });
delete this.data[0];
} else {
for(var i in files){
if(files[i].filename == result.filename && files[i].upload) {
files[i] = {
'id': result.id,
'name': result.name,
'filename': result.filename,
'url': this.get_file_url(result)
};
}
if (this.data[0] && this.data[0].filename == result.filename && this.data[0].upload) {
delete this.data[0];
this.data[result.id] = {
'id': result.id,
'name': result.name,
'filename': result.filename,
'url': this.get_file_url(result)
};
} else {
this.data[result.id] = {
'id': result.id,
'name': result.name,
'filename': result.filename,
'url': this.get_file_url(result)
};
}
var values = _.clone(this.get('value'));
values.push(result.id);
this.set({'value': values});
}
this.set({'value': files});
this.render_value()
},
on_file_delete: function (event) {
event.stopPropagation();
var file_id=$(event.target).data("id");
if (file_id) {
var files=[];
for(var i in this.get('value')){
if(file_id != this.get('value')[i].id){
files.push(this.get('value')[i]);
}
else if(!this.get('value')[i].no_unlink) {
this.ds_file.unlink([file_id]);
}
var files = _.filter(this.get('value'), function (id) {return id != file_id;});
if(!this.data[file_id].no_unlink) {
this.ds_file.unlink([file_id]);
}
this.set({'value': files});
}
@ -5216,8 +5263,20 @@ instance.web.form.FieldStatus = instance.web.form.AbstractField.extend({
this.options.clickable = this.options.clickable || (this.node.attrs || {}).clickable || false;
this.options.visible = this.options.visible || (this.node.attrs || {}).statusbar_visible || false;
this.set({value: false});
this.selection = [];
this.set("selection", []);
this.selection_dm = new instance.web.DropMisordered();
},
start: function() {
this.field_manager.on("view_content_has_changed", this, this.calc_domain);
this.calc_domain();
this.on("change:value", this, this.get_selection);
this.on("change:evaluated_selection_domain", this, this.get_selection);
this.on("change:selection", this, function() {
this.selection = this.get("selection");
this.render_value();
});
this.get_selection();
if (this.options.clickable) {
this.$el.on('click','li',this.on_click_stage);
}
@ -5234,15 +5293,25 @@ instance.web.form.FieldStatus = instance.web.form.AbstractField.extend({
},
render_value: function() {
var self = this;
self.get_selection().done(function() {
var content = QWeb.render("FieldStatus.content", {widget: self});
self.$el.html(content);
var colors = JSON.parse((self.node.attrs || {}).statusbar_colors || "{}");
var color = colors[self.get('value')];
if (color) {
self.$("oe_active").css("color", color);
}
});
var content = QWeb.render("FieldStatus.content", {widget: self});
self.$el.html(content);
var colors = JSON.parse((self.node.attrs || {}).statusbar_colors || "{}");
var color = colors[self.get('value')];
if (color) {
self.$("oe_active").css("color", color);
}
},
calc_domain: function() {
var d = instance.web.pyeval.eval('domain', this.build_domain());
var domain = []; //if there is no domain defined, fetch all the records
if (d.length) {
domain = ['|',['id', '=', this.get('value')]].concat(d);
}
if (! _.isEqual(domain, this.get("evaluated_selection_domain"))) {
this.set("evaluated_selection_domain", domain);
}
},
/** Get the selection and render it
* selection: [[identifier, value_to_display], ...]
@ -5251,32 +5320,37 @@ instance.web.form.FieldStatus = instance.web.form.AbstractField.extend({
*/
get_selection: function() {
var self = this;
self.selection = [];
if (this.field.type == "many2one") {
var domain = [];
if(!_.isEmpty(this.field.domain) || !_.isEmpty(this.node.attrs.domain)) {
var d = instance.web.pyeval.eval('domain', self.build_domain());
domain = ['|', ['id', '=', self.get('value')]].concat(d);
}
var ds = new instance.web.DataSetSearch(this, this.field.relation, self.build_context(), domain);
return ds.read_slice(['name'], {}).then(function (records) {
for(var i = 0; i < records.length; i++) {
self.selection.push([records[i].id, records[i].name]);
}
});
} else {
// For field type selection filter values according to
// statusbar_visible attribute of the field. For example:
// statusbar_visible="draft,open".
var selection = this.field.selection;
for(var i=0; i < selection.length; i++) {
var key = selection[i][0];
if(key == this.get('value') || !this.options.visible || this.options.visible.indexOf(key) != -1) {
this.selection.push(selection[i]);
var selection = [];
var calculation = _.bind(function() {
if (this.field.type == "many2one") {
var domain = [];
var ds = new instance.web.DataSetSearch(this, this.field.relation,
self.build_context(), this.get("evaluated_selection_domain"));
return ds.read_slice(['name'], {}).then(function (records) {
for(var i = 0; i < records.length; i++) {
selection.push([records[i].id, records[i].name]);
}
});
} else {
// For field type selection filter values according to
// statusbar_visible attribute of the field. For example:
// statusbar_visible="draft,open".
var select = this.field.selection;
for(var i=0; i < select.length; i++) {
var key = select[i][0];
if(key == this.get('value') || !this.options.visible || this.options.visible.indexOf(key) != -1) {
selection.push(select[i]);
}
}
return $.when();
}
return $.when();
}
}, this);
this.selection_dm.add(calculation()).then(function () {
if (! _.isEqual(selection, self.get("selection"))) {
self.set("selection", selection);
}
});
},
on_click_stage: function (ev) {
var self = this;
@ -5320,16 +5394,16 @@ instance.web.form.FieldMonetary = instance.web.form.FieldFloat.extend({
this.set({"currency_info": null});
return;
}
return this.ci_dm.add(new instance.web.Model("res.currency").query(["symbol", "position"])
.filter([["id", "=", self.get("currency")]]).first()).then(function(res) {
return this.ci_dm.add(self.alive(new instance.web.Model("res.currency").query(["symbol", "position"])
.filter([["id", "=", self.get("currency")]]).first())).then(function(res) {
self.set({"currency_info": res});
});
},
parse_value: function(val, def) {
return instance.web.parse_value(val, {type: "float"}, def);
return instance.web.parse_value(val, {type: "float", digits: (this.node.attrs || {}).digits || this.field.digits}, def);
},
format_value: function(val, def) {
return instance.web.format_value(val, {type: "float"}, def);
return instance.web.format_value(val, {type: "float", digits: (this.node.attrs || {}).digits || this.field.digits}, def);
},
});

View File

@ -321,9 +321,9 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi
.appendTo($this.empty())
.click(function (e) {e.stopPropagation();})
.append('<option value="80">80</option>' +
'<option value="100">100</option>' +
'<option value="200">200</option>' +
'<option value="500">500</option>' +
'<option value="2000">2000</option>' +
'<option value="NaN">' + _t("Unlimited") + '</option>')
.change(function () {
var val = parseInt($select.val(), 10);
@ -1186,7 +1186,7 @@ instance.web.ListView.List = instance.web.Class.extend( /** @lends instance.web.
}
});
instance.web.ListView.Groups = instance.web.Class.extend( /** @lends instance.web.ListView.Groups# */{
passtrough_events: 'action deleted row_link',
passthrough_events: 'action deleted row_link',
/**
* Grouped display for the ListView. Handles basic DOM events and interacts
* with the :js:class:`~DataGroup` bound to it.
@ -1406,7 +1406,7 @@ instance.web.ListView.Groups = instance.web.Class.extend( /** @lends instance.we
// can have selections spanning multiple links
var selection = self.get_selection();
$this.trigger(e, [selection.ids, selection.records]);
}).bind(this.passtrough_events, function (e) {
}).bind(this.passthrough_events, function (e) {
// additional positional parameters are provided to trigger as an
// Array, following the event type or event object, but are
// provided to the .bind event handler as *args.
@ -2212,7 +2212,7 @@ instance.web.list.Binary = instance.web.list.Column.extend({
if (value && value.substr(0, 10).indexOf(' ') == -1) {
download_url = "data:application/octet-stream;base64," + value;
} else {
download_url = this.session.url('/web/binary/saveas', {model: options.model, field: this.id, id: options.id});
download_url = instance.session.url('/web/binary/saveas', {model: options.model, field: this.id, id: options.id});
if (this.filename) {
download_url += '&filename_field=' + this.filename;
}

View File

@ -96,7 +96,8 @@ openerp.web.list_editable = function (instance) {
});
},
editable: function () {
return !this.options.disable_editable_mode
return !this.grouped
&& !this.options.disable_editable_mode
&& (this.fields_view.arch.attrs.editable
|| this._context_editable
|| this.options.editable);
@ -132,6 +133,15 @@ openerp.web.list_editable = function (instance) {
var self = this;
// tree/@editable takes priority on everything else if present.
var result = this._super(data, grouped);
// In case current editor was started previously, also has to run
// when toggling from editable to non-editable in case form widgets
// have setup global behaviors expecting themselves to exist
// somehow.
this.editor.destroy();
// Editor is not restartable due to formview not being restartable
this.editor = this.make_editor();
if (this.editable()) {
this.$el.addClass('oe_list_editable');
// FIXME: any hook available to ensure this is only done once?
@ -143,10 +153,6 @@ openerp.web.list_editable = function (instance) {
e.preventDefault();
self.cancel_edition();
});
this.editor.destroy();
// Editor is not restartable due to formview not being
// restartable
this.editor = this.make_editor();
var editor_ready = this.editor.prependTo(this.$el)
.done(this.proxy('setup_events'));
@ -795,7 +801,7 @@ openerp.web.list_editable = function (instance) {
});
instance.web.ListView.Groups.include(/** @lends instance.web.ListView.Groups# */{
passtrough_events: instance.web.ListView.Groups.prototype.passtrough_events + " edit saved",
passthrough_events: instance.web.ListView.Groups.prototype.passthrough_events + " edit saved",
get_row_for: function (record) {
return _(this.children).chain()
.invoke('get_row_for', record)

View File

@ -149,6 +149,9 @@ instance.web.ActionManager = instance.web.Widget.extend({
for (var i = 0; i < this.breadcrumbs.length; i += 1) {
var item = this.breadcrumbs[i];
var tit = item.get_title();
if (item.hide_breadcrumb) {
continue;
}
if (!_.isArray(tit)) {
tit = [tit];
}
@ -190,6 +193,18 @@ instance.web.ActionManager = instance.web.Widget.extend({
});
state = _.extend(params || {}, state);
}
if (this.inner_action.context) {
var active_id = this.inner_action.context.active_id;
if (active_id) {
state["active_id"] = active_id;
}
var active_ids = this.inner_action.context.active_ids;
if (active_ids && !(active_ids.length === 1 && active_ids[0] === active_id)) {
// We don't push active_ids if it's a single element array containing the active_id
// This makes the url shorter in most cases.
state["active_ids"] = this.inner_action.context.active_ids.join(',');
}
}
}
if(!this.dialog) {
this.getParent().do_push_state(state);
@ -212,8 +227,22 @@ instance.web.ActionManager = instance.web.Widget.extend({
} else {
var run_action = (!this.inner_widget || !this.inner_widget.action) || this.inner_widget.action.id !== state.action;
if (run_action) {
var add_context = {};
if (state.active_id) {
add_context.active_id = state.active_id;
}
if (state.active_ids) {
// The jQuery BBQ plugin does some parsing on values that are valid integers.
// It means that if there's only one item, it will do parseInt() on it,
// otherwise it will keep the comma seperated list as string.
add_context.active_ids = state.active_ids.toString().split(',').map(function(id) {
return parseInt(id, 10) || id;
});
} else if (state.active_id) {
add_context.active_ids = [state.active_id];
}
this.null_action();
action_loaded = this.do_action(state.action);
action_loaded = this.do_action(state.action, { additional_context: add_context });
$.when(action_loaded || null).done(function() {
instance.webclient.menu.has_been_loaded.done(function() {
if (self.inner_action && self.inner_action.id) {
@ -249,12 +278,27 @@ instance.web.ActionManager = instance.web.Widget.extend({
}
});
},
/**
* Execute an OpenERP action
*
* @param {Number|String|Object} Can be either an action id, a client action or an action descriptor.
* @param {Object} [options]
* @param {Boolean} [options.clear_breadcrumbs=false] Clear the breadcrumbs history list
* @param {Function} [options.on_reverse_breadcrumb] Callback to be executed whenever an anterior breadcrumb item is clicked on.
* @param {Function} [options.hide_breadcrumb] Do not display this widget's title in the breadcrumb
* @param {Function} [options.on_close] Callback to be executed when the dialog is closed (only relevant for target=new actions)
* @param {Function} [options.action_menu_id] Manually set the menu id on the fly.
* @param {Object} [options.additional_context] Additional context to be merged with the action's context.
* @return {jQuery.Deferred} Action loaded
*/
do_action: function(action, options) {
options = _.defaults(options || {}, {
clear_breadcrumbs: false,
on_reverse_breadcrumb: function() {},
hide_breadcrumb: false,
on_close: function() {},
action_menu_id: null,
additional_context: {},
});
if (action === false) {
action = { type: 'ir.actions.act_window_close' };
@ -269,9 +313,13 @@ instance.web.ActionManager = instance.web.Widget.extend({
}
// Ensure context & domain are evaluated and can be manipulated/used
if (action.context) {
action.context = instance.web.pyeval.eval(
'context', action.context);
var ncontext = new instance.web.CompoundContext(options.additional_context, action.context || {});
action.context = instance.web.pyeval.eval('context', ncontext);
if (action.context.active_id || action.context.active_ids) {
// Here we assume that when an `active_id` or `active_ids` is used
// in the context, we are in a `related` action, so we disable the
// searchview's default custom filters.
action.context.search_disable_custom_filters = true;
}
if (action.domain) {
action.domain = instance.web.pyeval.eval(
@ -360,7 +408,12 @@ instance.web.ActionManager = instance.web.Widget.extend({
widget: function () { return new instance.web.ViewManagerAction(self, action); },
action: action,
klass: 'oe_act_window',
post_process: function (widget) { widget.add_breadcrumb(options.on_reverse_breadcrumb); }
post_process: function (widget) {
widget.add_breadcrumb({
on_reverse_breadcrumb: options.on_reverse_breadcrumb,
hide_breadcrumb: options.hide_breadcrumb,
});
},
}, options);
},
ir_actions_client: function (action, options) {
@ -384,6 +437,7 @@ instance.web.ActionManager = instance.web.Widget.extend({
widget: widget,
title: action.name,
on_reverse_breadcrumb: options.on_reverse_breadcrumb,
hide_breadcrumb: options.hide_breadcrumb,
});
if (action.tag !== 'reload') {
self.do_push_state({});
@ -539,7 +593,7 @@ instance.web.ViewManager = instance.web.Widget.extend({
_.each(_.keys(self.views), function(view_name) {
var controller = self.views[view_name].controller;
if (controller) {
var container = self.$el.find(".oe_view_manager_view_" + view_name + ":first");
var container = self.$el.find("> .oe_view_manager_body > .oe_view_manager_view_" + view_name);
if (view_name === view_type) {
container.show();
controller.do_show(view_options || {});
@ -582,7 +636,7 @@ instance.web.ViewManager = instance.web.Widget.extend({
controller.on('switch_mode', self, this.switch_mode);
controller.on('previous_view', self, this.prev_view);
var container = this.$el.find(".oe_view_manager_view_" + view_type);
var container = this.$el.find("> .oe_view_manager_body > .oe_view_manager_view_" + view_type);
var view_promise = controller.appendTo(container);
this.views[view_type].controller = controller;
this.views[view_type].deferred.resolve(view_type);
@ -597,10 +651,24 @@ instance.web.ViewManager = instance.web.Widget.extend({
self.trigger("controller_inited",view_type,controller);
});
},
/**
* @returns {Number|Boolean} the view id of the given type, false if not found
*/
get_view_id: function(view_type) {
return this.views[view_type] && this.views[view_type].view_id || false;
},
set_title: function(title) {
this.$el.find('.oe_view_title_text:first').text(title);
},
add_breadcrumb: function(on_reverse_breadcrumb) {
add_breadcrumb: function(options) {
var options = options || {};
// 7.0 backward compatibility
if (typeof options == 'function') {
options = {
on_reverse_breadcrumb: options
};
}
// end of 7.0 backward compatibility
var self = this;
var views = [this.active_view || this.views_src[0].view_type];
this.on('switch_mode', self, function(mode) {
@ -612,7 +680,7 @@ instance.web.ViewManager = instance.web.Widget.extend({
views.push(mode);
}
});
this.getParent().push_breadcrumb({
var item = _.extend({
widget: this,
action: this.action,
show: function(index) {
@ -646,9 +714,9 @@ instance.web.ViewManager = instance.web.Widget.extend({
titles.pop();
}
return titles;
},
on_reverse_breadcrumb: on_reverse_breadcrumb,
});
}
}, options);
this.getParent().push_breadcrumb(item);
},
/**
* Returns to the view preceding the caller view in this manager's
@ -953,10 +1021,11 @@ instance.web.ViewManagerAction = instance.web.ViewManager.extend({
});
},
do_create_view: function(view_type) {
var r = this._super.apply(this, arguments);
var view = this.views[view_type].controller;
view.set({ 'title': this.action.name });
return r;
var self = this;
return this._super.apply(this, arguments).then(function() {
var view = self.views[view_type].controller;
view.set({ 'title': self.action.name });
});
},
get_action_manager: function() {
var cur = this;
@ -1213,6 +1282,7 @@ instance.web.View = instance.web.Widget.extend({
"view_id": this.view_id,
"view_type": this.view_type,
"toolbar": !!this.options.$sidebar,
"context": this.dataset.get_context(),
});
}
return view_loaded_def.then(function(r) {
@ -1270,11 +1340,6 @@ instance.web.View = instance.web.Widget.extend({
active_ids: [record_id],
active_model: dataset.model
});
if (("" + action.context).match(/\bactive_id\b/)) {
// Special case: when the context is evaluted using
// the active_id, we want to disable the custom filters.
ncontext.add({ search_disable_custom_filters: true });
}
}
ncontext.add(action.context || {});
action.context = ncontext;
@ -1401,11 +1466,11 @@ instance.web.View = instance.web.Widget.extend({
* Performs a fields_view_get and apply postprocessing.
* return a {$.Deferred} resolved with the fvg
*
* @param {Object} [args]
* @param {Object} args
* @param {String|Object} args.model instance.web.Model instance or string repr of the model
* @param {null|Object} args.context context if args.model is a string
* @param {null|Number} args.view_id id of the view to be loaded, default view if null
* @param {null|String} args.view_type type of view to be loaded if view_id is null
* @param {Object} [args.context] context if args.model is a string
* @param {Number} [args.view_id] id of the view to be loaded, default view if null
* @param {String} [args.view_type] type of view to be loaded if view_id is null
* @param {Boolean} [args.toolbar=false] get the toolbar definition
*/
instance.web.fields_view_get = function(args) {
@ -1432,7 +1497,7 @@ instance.web.fields_view_get = function(args) {
if (typeof model === 'string') {
model = new instance.web.Model(args.model, args.context);
}
return args.model.call('fields_view_get', [args.view_id, args.view_type, model.context(), args.toolbar]).then(function(fvg) {
return args.model.call('fields_view_get', [args.view_id, args.view_type, args.context, args.toolbar]).then(function(fvg) {
return postprocess(fvg);
});
};
@ -1502,13 +1567,27 @@ instance.web.json_node_to_xml = function(node, human_readable, indent) {
}
};
instance.web.xml_to_str = function(node) {
var str = "";
if (window.XMLSerializer) {
return (new XMLSerializer()).serializeToString(node);
str = (new XMLSerializer()).serializeToString(node);
} else if (window.ActiveXObject) {
return node.xml;
str = node.xml;
} else {
throw new Error(_t("Could not serialize XML"));
}
// Browsers won't deal with self closing tags except void elements:
// http://www.w3.org/TR/html-markup/syntax.html
var void_elements = 'area base br col command embed hr img input keygen link meta param source track wbr'.split(' ');
// The following regex is a bit naive but it's ok for the xmlserializer output
str = str.replace(/<([a-z]+)([^<>]*)\s*\/\s*>/g, function(match, tag, attrs) {
if (void_elements.indexOf(tag) < 0) {
return "<" + tag + attrs + "></" + tag + ">";
} else {
return match;
}
});
return str;
};
/**

View File

@ -40,7 +40,7 @@
<td>
<p>
<t t-js="d">
var message = d.message ? d.message : d.error.data.message;
var message = d.message ? d.message : d.error.data.fault_code;
d.html_error = context.engine.tools.html_escape(message)
.replace(/\n/g, '<br/>');
</t>
@ -71,9 +71,9 @@
</div>
<ul>
<li>Username</li>
<li><input name="login" type="text" t-att-value="widget.selected_login || ''" autofocus="autofocus"/></li>
<li><input name="login" type="text" value="" autofocus="autofocus"/></li>
<li>Password</li>
<li><input name="password" type="password" t-att-value="widget.selected_password || ''"/></li>
<li><input name="password" type="password" value=""/></li>
<li><button name="submit">Log in</button></li>
</ul>
</form>
@ -106,33 +106,47 @@
<div class="oe_view_manager_header" style="padding: 8px;">
<div class="oe_header_row">
<h2 class="oe_view_title">
<span class="oe_view_title_text oe_breadcrumb_title">Create Database</span>
<span class="oe_view_title_text oe_breadcrumb_title">Create a New Database</span>
</h2>
<button type="submit" class="oe_button oe_highlight db_create">Create</button>
</div>
</div>
</div>
<table align="center" class="db_option_table">
<p class="oe_grey" style="margin: 10px">
Fill in this form to create an OpenERP database. You can
create databases for different companies or for different
goals (testing, production). Once the database is created,
you will be able to install your first application.
</p>
<p class="oe_grey" style="margin: 10px">
By default, the master password is 'admin'. This password
is required to created, delete dump or restore databases.
</p>
<table class="db_option_table" style="margin: 10px">
<tr>
<td><label for="super_admin_pwd">Master password:</label></td>
<td><input type="password" name="super_admin_pwd" class="required" value="admin" /></td>
<td>
<input type="password" name="super_admin_pwd" class="required" value="admin"/>
</td>
</tr>
<tr>
<td><label for="db_name">New database name:</label></td>
<td><input type="text" name="db_name" class="required" matches="^[a-zA-Z][a-zA-Z0-9_-]+$" autofocus="true"/></td>
<td><label for="db_name">Select a database name:</label></td>
<td>
<input type="text" name="db_name" class="required" matches="^[a-zA-Z0-9][a-zA-Z0-9_-]+$" autofocus="true" placeholder="e.g. mycompany"/>
</td>
</tr>
<tr>
<td><label for="demo_data">Load Demonstration data:</label></td>
<td><label for="demo_data">Load demonstration data:</label></td>
<td class="oe_form_group_cell">
<span class="oe_form_field oe_form_field_boolean">
<span class="oe_form_field oe_form_field_boolean oe_grey" >
<input type="checkbox" name="demo_data" />
Check this box to evaluate OpenERP.
</span>
</td>
</tr>
<tr>
<td><label for="db_lang">Default language:</label></td>
<td class="oe_form_field oe_form_field_selection ">
<select name="db_lang" t-if="widget.lang_list">
<td class="oe_form_field oe_form_field_selection">
<select name="db_lang" t-if="widget.lang_list" class="oe_inline">
<t t-foreach="widget.lang_list" t-as="lang">
<option t-att-value="lang[0]" t-att-selected="lang[0] === 'en_US' ? 'selected' : undefined">
<t t-esc="lang[1]" />
@ -142,13 +156,17 @@
</td>
</tr>
<tr>
<td><label for="create_admin_pwd">Admin password:</label></td>
<td><label for="create_admin_pwd">Choose a password:</label></td>
<td><input type="password" name="create_admin_pwd" class="required" /></td>
</tr>
<tr>
<td><label for="create_confirm_pwd">Confirm password:</label></td>
<td><input type="password" name="create_confirm_pwd" class="required" equalTo="input[name=create_admin_pwd]"/></td>
</tr>
<tr>
<td></td>
<td><button type="submit" class="oe_button oe_highlight db_create">Create Database</button></td>
</tr>
</table>
</form>
<form id="db_duplicate" name="duplicate_db_form" style="display: none;">
@ -403,8 +421,9 @@
<img class="oe_topbar_avatar" t-att-data-default-src="_s + '/web/static/src/img/user_menu_avatar.png'"/>
<span class="oe_topbar_name"/>
<ul class="oe_dropdown_menu">
<li><a href="#" data-menu="about">About OpenERP</a></li>
<li><a href="#" data-menu="settings">Preferences</a></li>
<li><a href="#" data-menu="about">About OpenERP</a></li>
<li><a href="#" data-menu="help">Help</a></li>
<li><a href="#" data-menu="logout">Log out</a></li>
</ul>
</span>
@ -1031,17 +1050,22 @@
</t>
<t t-name="FieldText">
<div class="oe_form_field oe_form_field_text" t-att-style="widget.node.attrs.style">
<textarea rows="6"
t-att-name="widget.name"
class="field_text"
t-att-tabindex="widget.node.attrs.tabindex"
t-att-autofocus="widget.node.attrs.autofocus"
t-att-placeholder="! widget.get('effective_readonly') ? widget.node.attrs.placeholder : ''"
t-att-maxlength="widget.field.size"
></textarea><img class="oe_field_translate oe_input_icon"
t-if="widget.field.translate and !widget.get('effective_readonly')"
t-att-src='_s + "/web/static/src/img/icons/terp-translate.png"' width="16" height="16" border="0"
/>
<t t-if="!widget.get('effective_readonly')">
<textarea rows="6"
t-att-name="widget.name"
class="field_text"
t-att-tabindex="widget.node.attrs.tabindex"
t-att-autofocus="widget.node.attrs.autofocus"
t-att-placeholder="! widget.get('effective_readonly') ? widget.node.attrs.placeholder : ''"
t-att-maxlength="widget.field.size"
></textarea><img class="oe_field_translate oe_input_icon"
t-if="widget.field.translate and !widget.get('effective_readonly')"
t-att-src='_s + "/web/static/src/img/icons/terp-translate.png"' width="16" height="16" border="0"
/>
</t>
<t t-if="widget.get('effective_readonly')">
<span class="oe_form_text_content"></span>
</t>
</div>
</t>
<t t-name="FieldTextHtml">
@ -1060,8 +1084,9 @@
t-att-name="widget.name"
t-att-placeholder="placeholder"
class="oe_datepicker_master"
/><img class="oe_input_icon oe_datepicker_trigger" t-att-src='_s + "/web/static/src/img/ui/field_calendar.png"'
title="Select date" width="16" height="16" border="0"/>
/><img class="oe_input_icon oe_datepicker_trigger" draggable="false"
t-att-src='_s + "/web/static/src/img/ui/field_calendar.png"'
title="Select date" width="16" height="16" border="0"/>
</span>
</t>
<t t-name="FieldDate">
@ -1095,7 +1120,7 @@
</t>
<t t-if="!widget.get('effective_readonly')">
<a t-if="! widget.options.no_open" href="#" tabindex="-1"
class="oe_m2o_cm_button oe_e">/</a>
class="oe_m2o_cm_button oe_e" draggable="false">/</a>
<div>
<input type="text"
t-att-id="widget.id_for_label"
@ -1104,7 +1129,7 @@
t-att-placeholder="widget.node.attrs.placeholder"
/>
<span class="oe_m2o_drop_down_button">
<img t-att-src='_s + "/web/static/src/img/down-arrow.png"'/>
<img t-att-src='_s + "/web/static/src/img/down-arrow.png"' draggable="false"/>
</span>
</div>
</t>
@ -1265,8 +1290,9 @@
</t>
<t t-name="FieldBinaryFileUploader.files">
<div class="oe_attachments">
<t t-if="widget.get('value')">
<t t-if="!widget.get('effective_readonly')" t-foreach="widget.get('value')" t-as="file">
<t t-if="!widget.get('effective_readonly')">
<t t-foreach="widget.get('value')" t-as="id">
<t t-set="file" t-value="widget.data[id]"/>
<div class="oe_attachment">
<span t-if="(file.upload or file.percent_loaded&lt;100)" t-attf-title="{(file.name || file.filename) + (file.date?' \n('+file.date+')':'' )}" t-attf-name="{file.name || file.filename}">
<span class="oe_fileuploader_in_process">...Upload in progress...</span>
@ -1280,7 +1306,10 @@
</t>
</div>
</t>
<t t-if="widget.get('effective_readonly')" t-foreach="widget.get('value')" t-as="file">
</t>
<t t-if="widget.get('effective_readonly')">
<t t-foreach="widget.get('value')" t-as="id">
<t t-set="file" t-value="widget.data[id]"/>
<div>
<a t-att-href="file.url" t-attf-title="{(file.name || file.filename) + (file.date?' \n('+file.date+')':'' )}">
<t t-raw="file.name || file.filename"/>
@ -1296,10 +1325,10 @@
<div class="oe_add" t-if="!widget.get('effective_readonly')">
<!-- uploader of file -->
<button class="oe_attach"><span class="oe_e">'</span></button>
<span class='oe_attach_label'>File</span>
<span class='oe_attach_label'><t t-esc="widget.string"/></span>
<t t-call="HiddenInputFile">
<t t-set="fileupload_id" t-value="widget.fileupload_id"/>
<t t-set="fileupload_action">/web/binary/upload_attachment</t>
<t t-set="fileupload_action" t-translation="off">/web/binary/upload_attachment</t>
<input type="hidden" name="model" t-att-value="widget.view.model"/>
<input type="hidden" name="id" value="0"/>
<input type="hidden" name="session_id" t-att-value="widget.session.session_id"/>
@ -1495,7 +1524,7 @@
<t t-esc="attrs.string"/>
</button>
<ul t-name="SearchView.filters">
<li t-foreach="widget.filters" t-as="filter"
<li t-foreach="widget.filters" t-as="filter" t-if="filter.visible()"
t-att-title="filter.attrs.string ? filter.attrs.help : undefined">
<t t-esc="filter.attrs.string or filter.attrs.help or filter.attrs.name or 'Ω'"/>
</li>
@ -1583,15 +1612,6 @@
</div>
</div>
</t>
<t t-name="SearchView.group">
<t t-call="SearchView.util.expand">
<t t-set="expand" t-value="attrs.expand"/>
<t t-set="label" t-value="attrs.string"/>
<t t-set="content">
<t t-call="SearchView.render_lines"/>
</t>
</t>
</t>
<div t-name="SearchView.Filters" class="oe_searchview_filters oe_searchview_section">
</div>
@ -1822,7 +1842,7 @@
<tr>
<td t-foreach="records[0]" t-as="column">
<input class="sel_fields" placeholder="--- Don't Import ---"/><span class="oe_m2o_drop_down_button">
<img t-att-src='_s + "/web/static/src/img/down-arrow.png"' /></span>
<img t-att-src='_s + "/web/static/src/img/down-arrow.png"' draggable="false"/></span>
</td>
</tr>
<tr t-foreach="records" t-as="record" class="oe_import_grid-row">

View File

@ -259,6 +259,12 @@ openerp.testing.section('eval.types', {
py.eval('a + a', ctx);
}, /^Error: TypeError:/);
});
test('relastivedelta', function (instance) {
strictEqual(
py.eval("(datetime.date(2012, 2, 15) + relativedelta(days=-1)).strftime('%Y-%m-%d 23:59:59')",
instance.web.pyeval.context()),
"2012-02-14 23:59:59");
})
});
openerp.testing.section('eval.edc', {
dependencies: ['web.data'],

View File

@ -1,4 +1,4 @@
openerp.testing.section('query', {
openerp.testing.section('search.query', {
dependencies: ['web.search']
}, function (test) {
test('Adding a facet to the query creates a facet and a value', function (instance) {
@ -180,7 +180,7 @@ var makeSearchView = function (instance, dummy_widget_attributes, defaults) {
});
return view;
};
openerp.testing.section('defaults', {
openerp.testing.section('search.defaults', {
dependencies: ['web.search'],
rpc: 'mock',
templates: true,
@ -318,6 +318,28 @@ openerp.testing.section('defaults', {
"facet value should match provided default's selection");
});
});
test("M2O default: value array", {asserts: 2}, function (instance, $s, mock) {
var view = {inputs: []}, id = 5;
var f = new instance.web.search.ManyToOneField(
{attrs: {name: 'dummy', string: 'Dummy'}},
{relation: 'dummy.model.name'},
view);
mock('dummy.model.name:name_get', function (args) {
equal(args[0], id);
return [[id, "DumDumDum"]];
});
return f.facet_for_defaults({dummy: [id]})
.done(function (facet) {
var model = facet;
if (!(model instanceof instance.web.search.Facet)) {
model = new instance.web.search.Facet(facet);
}
deepEqual(
model.values.toJSON(),
[{label: "DumDumDum", value: id}],
"should support default as a singleton");
});
});
test("M2O default: value", {asserts: 1}, function (instance, $s, mock) {
var view = {inputs: []}, id = 4;
var f = new instance.web.search.ManyToOneField(
@ -330,8 +352,17 @@ openerp.testing.section('defaults', {
ok(!facet, "an invalid m2o default should yield a non-facet");
});
});
test("M2O default: values", {rpc: false}, function (instance) {
var view = {inputs: []};
var f = new instance.web.search.ManyToOneField(
{attrs: {name: 'dummy', string: 'Dummy'}},
{relation: 'dummy.model.name'},
view);
raises(function () { f.facet_for_defaults({dummy: [6, 7]}) },
"should not accept multiple default values");
})
});
openerp.testing.section('completions', {
openerp.testing.section('search.completions', {
dependencies: ['web.search'],
rpc: 'mock',
templates: true
@ -526,7 +557,7 @@ openerp.testing.section('completions', {
return [[42, "choice 1"], [43, "choice @"]];
});
var view = {inputs: []};
var view = {inputs: [], dataset: {get_context: function () {}}};
var f = new instance.web.search.ManyToOneField(
{attrs: {string: 'Dummy'}}, {relation: 'dummy.model'}, view);
return f.complete("bob")
@ -555,7 +586,7 @@ openerp.testing.section('completions', {
strictEqual(kwargs.name, 'bob');
return [];
});
var view = {inputs: []};
var view = {inputs: [], dataset: {get_context: function () {}}};
var f = new instance.web.search.ManyToOneField(
{attrs: {string: 'Dummy'}}, {relation: 'dummy.model'}, view);
return f.complete("bob")
@ -563,8 +594,28 @@ openerp.testing.section('completions', {
ok(!c, "no match should yield no completion");
});
});
test("M2O filtered", {asserts: 2}, function (instance, $s, mock) {
mock('dummy.model:name_search', function (args, kwargs) {
deepEqual(args, [], "should have no positional arguments");
deepEqual(kwargs, {
name: 'bob',
limit: 8,
args: [['foo', '=', 'bar']],
context: {flag: 1},
}, "should use filtering domain");
return [[42, "Match"]];
});
var view = {
inputs: [],
dataset: {get_context: function () { return {flag: 1}; }}
};
var f = new instance.web.search.ManyToOneField(
{attrs: {string: 'Dummy', domain: '[["foo", "=", "bar"]]'}},
{relation: 'dummy.model'}, view);
return f.complete("bob");
});
});
openerp.testing.section('search-serialization', {
openerp.testing.section('search.serialization', {
dependencies: ['web.search'],
rpc: 'mock',
templates: true
@ -804,11 +855,11 @@ openerp.testing.section('search-serialization', {
test('FilterGroup', {asserts: 6}, function (instance) {
var view = {inputs: [], query: {on: function () {}}};
var filter_a = new instance.web.search.Filter(
{attrs: {name: 'a', context: 'c1', domain: 'd1'}}, view);
{attrs: {name: 'a', context: '{"c1": True}', domain: 'd1'}}, view);
var filter_b = new instance.web.search.Filter(
{attrs: {name: 'b', context: 'c2', domain: 'd2'}}, view);
{attrs: {name: 'b', context: '{"c2": True}', domain: 'd2'}}, view);
var filter_c = new instance.web.search.Filter(
{attrs: {name: 'c', context: 'c3', domain: 'd3'}}, view);
{attrs: {name: 'c', context: '{"c3": True}', domain: 'd3'}}, view);
var group = new instance.web.search.FilterGroup(
[filter_a, filter_b, filter_c], view);
return group.facet_for_defaults({a: true, c: true})
@ -829,7 +880,7 @@ openerp.testing.section('search-serialization', {
equal(context.__ref, 'compound_context',
"context should be compound");
deepEqual(context.__contexts, [
'c1', 'c3'
'{"c1": True}', '{"c3": True}'
], "context should merge all filter contexts");
ok(!context.get_eval_context(), "context should have no evaluation context");
});
@ -871,7 +922,7 @@ openerp.testing.section('search-serialization', {
return $.when(t1, t2);
});
});
openerp.testing.section('removal', {
openerp.testing.section('search.removal', {
dependencies: ['web.search'],
rpc: 'mock',
templates: true
@ -894,7 +945,7 @@ openerp.testing.section('removal', {
});
});
});
openerp.testing.section('drawer', {
openerp.testing.section('search.drawer', {
dependencies: ['web.search'],
rpc: 'mock',
templates: true
@ -910,7 +961,7 @@ openerp.testing.section('drawer', {
});
});
});
openerp.testing.section('filters', {
openerp.testing.section('search.filters', {
dependencies: ['web.search'],
rpc: 'mock',
templates: true,
@ -995,7 +1046,104 @@ openerp.testing.section('filters', {
});
});
});
openerp.testing.section('saved_filters', {
openerp.testing.section('search.groupby', {
dependencies: ['web.search'],
rpc: 'mock',
templates: true,
}, function (test) {
test('basic', {
asserts: 7,
setup: function (instance, $s, mock) {
mock('dummy.model:fields_view_get', function () {
return {
type: 'search',
fields: {},
arch: [
'<search>',
'<filter string="Foo" context="{\'group_by\': \'foo\'}"/>',
'<filter string="Bar" context="{\'group_by\': \'bar\'}"/>',
'<filter string="Baz" context="{\'group_by\': \'baz\'}"/>',
'</search>'
].join(''),
}
});
}
}, function (instance, $fix) {
var view = makeSearchView(instance);
return view.appendTo($fix)
.done(function () {
// 3 filters, 1 filtergroup group, 1 custom filter, 1 advanced, 1 Filters
equal(view.inputs.length, 7,
'should have 7 inputs total');
var group = _.find(view.inputs, function (f) {
return f instanceof instance.web.search.GroupbyGroup
});
ok(group, "should have a GroupbyGroup input");
strictEqual(group.getParent(), view,
"group's parent should be view");
group.toggle(group.filters[0]);
group.toggle(group.filters[2]);
var results = view.build_search_data();
deepEqual(results.errors, [], "should have no errors");
deepEqual(results.domains, [], "should have no domain");
deepEqual(results.contexts, [
new instance.web.CompoundContext(
"{'group_by': 'foo'}", "{'group_by': 'baz'}")
], "should have compound contexts");
deepEqual(results.groupbys, [
"{'group_by': 'foo'}",
"{'group_by': 'baz'}"
], "should have sequence of contexts")
});
});
test('unified multiple groupby groups', {
asserts: 4,
setup: function (instance, $s, mock) {
mock('dummy.model:fields_view_get', function () {
return {
type: 'search',
fields: {},
arch: [
'<search>',
'<filter string="Foo" context="{\'group_by\': \'foo\'}"/>',
'<separator/>',
'<filter string="Bar" context="{\'group_by\': \'bar\'}"/>',
'<separator/>',
'<filter string="Baz" context="{\'group_by\': \'baz\'}"/>',
'</search>'
].join(''),
}
});
}
}, function (instance, $fix) {
var view = makeSearchView(instance);
return view.appendTo($fix)
.done(function () {
// 3 filters, 3 filtergroups, 1 custom filter, 1 advanced, 1 Filters
equal(view.inputs.length, 9, "should have 9 inputs total");
var groups = _.filter(view.inputs, function (f) {
return f instanceof instance.web.search.GroupbyGroup
});
equal(groups.length, 3, "should have 3 GroupbyGroups");
groups[0].toggle(groups[0].filters[0]);
groups[2].toggle(groups[2].filters[0]);
equal(view.query.length, 1,
"should have unified groupby groups in single facet");
deepEqual(view.build_search_data(), {
errors: [],
domains: [],
contexts: [new instance.web.CompoundContext(
"{'group_by': 'foo'}", "{'group_by': 'baz'}")],
groupbys: [ "{'group_by': 'foo'}", "{'group_by': 'baz'}" ],
}, "should only have contexts & groupbys in search data");
});
});
});
openerp.testing.section('search.filters.saved', {
dependencies: ['web.search'],
rpc: 'mock',
templates: true
@ -1038,8 +1186,68 @@ openerp.testing.section('saved_filters', {
"should not be checked anymore");
});
});
test('toggling', {asserts: 2}, function (instance, $fix, mock) {
var view = makeSearchView(instance);
mock('ir.filters:get_filters', function () {
return [{name: 'filter name', user_id: 42, id: 1}];
});
return view.appendTo($fix)
.done(function () {
var $row = $fix.find('.oe_searchview_custom li:first').click();
equal(view.query.length, 1, "should have one facet");
$row.click();
equal(view.query.length, 0, "should have removed facet");
});
});
test('replacement', {asserts: 4}, function (instance, $fix, mock) {
var view = makeSearchView(instance);
mock('ir.filters:get_filters', function () {
return [
{name: 'f', user_id: 42, id: 1, context: {'private': 1}},
{name: 'f', user_id: false, id: 2, context: {'private': 0}}
];
});
return view.appendTo($fix)
.done(function () {
$fix.find('.oe_searchview_custom li:eq(0)').click();
equal(view.query.length, 1, "should have one facet");
deepEqual(
view.query.at(0).get('field').get_context(),
{'private': 1},
"should have selected first filter");
$fix.find('.oe_searchview_custom li:eq(1)').click();
equal(view.query.length, 1, "should have one facet");
deepEqual(
view.query.at(0).get('field').get_context(),
{'private': 0},
"should have selected second filter");
});
});
test('creation', {asserts: 2}, function (instance, $fix, mock) {
// force a user context
instance.session.user_context = {foo: 'bar'};
var view = makeSearchView(instance);
var done = $.Deferred();
mock('ir.filters:get_filters', function () { return []; });
mock('ir.filters:create_or_replace', function (args) {
var filter = args[0];
deepEqual(filter.context, {}, "should have empty context");
deepEqual(filter.domain, [], "should have empty domain");
done.resolve();
});
return view.appendTo($fix)
.then(function () {
$fix.find('.oe_searchview_custom input#oe_searchview_custom_input')
.text("filter name")
.end()
.find('.oe_searchview_custom button').click();
return done.promise();
});
});
});
openerp.testing.section('advanced', {
openerp.testing.section('search.advanced', {
dependencies: ['web.search'],
rpc: 'mock',
templates: true
@ -1120,3 +1328,159 @@ openerp.testing.section('advanced', {
});
// TODO: UI tests?
});
openerp.testing.section('search.invisible', {
dependencies: ['web.search'],
rpc: 'mock',
templates: true,
}, function (test) {
var registerTestField = function (instance, methods) {
instance.web.search.fields.add('test', 'instance.testing.TestWidget');
instance.testing = {
TestWidget: instance.web.search.Field.extend(methods),
};
};
var makeView = function (instance, mock, fields, arch, defaults) {
mock('ir.filters:get_filters', function () { return []; });
mock('test.model:fields_get', function () { return fields; });
mock('test.model:fields_view_get', function () {
return { type: 'search', fields: fields, arch: arch };
});
var ds = new instance.web.DataSet(null, 'test.model');
return new instance.web.SearchView(null, ds, false, defaults);
};
// Invisible fields should not auto-complete
test('invisible-field-no-autocomplete', {asserts: 1}, function (instance, $fix, mock) {
registerTestField(instance, {
complete: function () {
return $.when([{label: this.attrs.string}]);
},
});
var view = makeView(instance, mock, {
field0: {type: 'test', string: 'Field 0'},
field1: {type: 'test', string: 'Field 1'},
}, ['<search>',
'<field name="field0"/>',
'<field name="field1" modifiers="{&quot;invisible&quot;: true}"/>',
'</search>'].join());
return view.appendTo($fix)
.then(function () {
var done = $.Deferred();
view.complete_global_search({term: 'test'}, function (comps) {
done.resolve(comps);
});
return done;
}).then(function (completions) {
deepEqual(completions, [{label: 'Field 0'}],
"should only complete the visible field");
});
});
// Invisible filters should not appear in the drawer
test('invisible-filter-no-drawer', {asserts: 4}, function (instance, $fix, mock) {
var view = makeView(instance, mock, {}, [
'<search>',
'<filter string="filter 0"/>',
'<filter string="filter 1" modifiers="{&quot;invisible&quot;: true}"/>',
'</search>'].join());
return view.appendTo($fix)
.then(function () {
var $fs = $fix.find('.oe_searchview_filters ul');
strictEqual($fs.children().length,
1,
"should only display one filter");
strictEqual(_.str.trim($fs.children().text()),
"filter 0",
"should only display filter 0");
var done = $.Deferred();
view.complete_global_search({term: 'filter'}, function (comps) {
done.resolve();
strictEqual(comps.length, 1, "should only complete visible filter");
strictEqual(comps[0].label, "Filter on: filter 0",
"should complete filter 0");
});
return done;
});
});
// Invisible filter groups should not appear in the drawer
// Group invisibility should be inherited by children
test('group-invisibility', {asserts: 6}, function (instance, $fix, mock) {
registerTestField(instance, {
complete: function () {
return $.when([{label: this.attrs.string}]);
},
});
var view = makeView(instance, mock, {
field0: {type: 'test', string: 'Field 0'},
field1: {type: 'test', string: 'Field 1'},
}, [
'<search>',
'<group string="Visibles">',
'<field name="field0"/>',
'<filter string="Filter 0"/>',
'</group>',
'<group string="Invisibles" modifiers="{&quot;invisible&quot;: true}">',
'<field name="field1"/>',
'<filter string="Filter 1"/>',
'</group>',
'</search>'
].join(''));
return view.appendTo($fix)
.then(function () {
strictEqual($fix.find('.oe_searchview_filters h3').length,
1,
"should only display one group");
strictEqual($fix.find('.oe_searchview_filters h3').text(),
'w Visibles',
"should only display the Visibles group (and its icon char)");
var $fs = $fix.find('.oe_searchview_filters ul');
strictEqual($fs.children().length, 1,
"should only have one filter in the drawer");
strictEqual(_.str.trim($fs.text()), "Filter 0",
"should have filter 0 as sole filter");
var done = $.Deferred();
view.complete_global_search({term: 'filter'}, function (compls) {
done.resolve();
strictEqual(compls.length, 2,
"should have 2 completions");
deepEqual(_.pluck(compls, 'label'),
['Field 0', 'Filter on: Filter 0'],
"should complete on field 0 and filter 0");
});
return done;
});
});
// Default on invisible fields should still work, for fields and filters both
test('invisible-defaults', {asserts: 1}, function (instance, $fix, mock) {
var view = makeView(instance, mock, {
field: {type: 'char', string: "Field"},
field2: {type: 'char', string: "Field 2"},
}, [
'<search>',
'<field name="field2"/>',
'<filter name="filter2" string="Filter"',
' domain="[[\'qwa\', \'=\', 42]]"/>',
'<group string="Invisibles" modifiers="{&quot;invisible&quot;: true}">',
'<field name="field"/>',
'<filter name="filter" string="Filter"',
' domain="[[\'whee\', \'=\', \'42\']]"/>',
'</group>',
'</search>'
].join(''), {field: "foo", filter: true});
return view.appendTo($fix)
.then(function () {
deepEqual(view.build_search_data(), {
errors: [],
groupbys: [],
contexts: [],
domains: [
// Generated from field
[['field', 'ilike', 'foo']],
// generated from filter
"[['whee', '=', '42']]"
],
}, "should yield invisible fields selected by defaults");
});
});
});

View File

@ -18,7 +18,7 @@
.openerp .oe_calendar .dhx_cal_select_menu .dhx_menu_icon.icon_edit {
display: none;
}
.openerp .oe_calendar .dhx_cal_navline {
.openerp .oe_calendar .dhx_cal_navline, .openerp .oe_calendar .dhx_cal_header {
z-index: auto;
}
.openerp .oe_calendar.oe_cal_month .dhx_cal_data table tr td:last-child div.dhx_month_body {

View File

@ -20,7 +20,7 @@
// Dhtmlx Scheduler css overrides
.dhx_cal_select_menu .dhx_menu_icon.icon_edit
display: none
.dhx_cal_navline
.dhx_cal_navline, .dhx_cal_header
// dhtmlx sets the index to 3 (in glossy theme)
// I didn't found the reason yet but it overlaps the
// dropdown menus and I want to avoid a z-index war.

View File

@ -414,6 +414,20 @@ instance.web_calendar.CalendarView = instance.web.View.extend({
self.slow_create(event_id, event_obj);
});
},
get_form_popup_infos: function() {
var parent = this.getParent();
var infos = {
view_id: false,
title: this.name,
};
if (parent instanceof instance.web.ViewManager) {
infos.view_id = parent.get_view_id('form');
if (parent instanceof instance.web.ViewManagerAction && parent.action && parent.action.name) {
infos.title = parent.action.name;
}
}
return infos;
},
slow_create: function(event_id, event_obj) {
var self = this;
if (this.current_mode() === 'month') {
@ -431,9 +445,11 @@ instance.web_calendar.CalendarView = instance.web.View.extend({
});
var something_saved = false;
var pop = new instance.web.form.FormOpenPopup(this);
var pop_infos = this.get_form_popup_infos();
pop.show_element(this.dataset.model, null, this.dataset.get_context(defaults), {
title: _t("Create: ") + ' ' + this.name,
title: _.str.sprintf(_t("Create: %s"), pop_infos.title),
disable_multiple_selection: true,
view_id: pop_infos.view_id,
});
pop.on('closed', self, function() {
if (!something_saved) {
@ -465,9 +481,11 @@ instance.web_calendar.CalendarView = instance.web.View.extend({
});
} else {
var pop = new instance.web.form.FormOpenPopup(this);
var pop_infos = this.get_form_popup_infos();
var id_from_dataset = this.dataset.ids[index]; // dhtmlx scheduler loses id's type
pop.show_element(this.dataset.model, id_from_dataset, this.dataset.get_context(), {
title: _t("Edit: ") + this.name
title: _.str.sprintf(_t("Edit: %s"), pop_infos.title),
view_id: pop_infos.view_id,
});
pop.on('write_completed', self, function(){
self.reload_event(id_from_dataset);

View File

@ -386,6 +386,7 @@ function GanttChart()
this.hourInPixelsWork = this.dayInPixels / this.hoursInDay;
this.hourInPixels = this.dayInPixels / 24;
this.minWidthResize = this.hourInPixels;
this.countDays = 0;
this.startDate = null;
this.initialPos = 0;
@ -4812,7 +4813,7 @@ GanttTask.prototype.getResizeInfo = function()
childParentPosX = posChildTaskItem;
}
this.minWidthResize = this.Chart.dayInPixels;
this.minWidthResize = this.Chart.minWidthResize;
if (this.childTask.length > 0)
{

View File

@ -24,8 +24,8 @@ instance.web_gantt.GanttView = instance.web.View.extend({
var self = this;
this.fields_view = fields_view_get;
this.$el.addClass(this.fields_view.arch.attrs['class']);
return new instance.web.Model(this.dataset.model)
.call('fields_get').then(function (fields) {
return self.alive(new instance.web.Model(this.dataset.model)
.call('fields_get')).then(function (fields) {
self.fields = fields;
self.has_been_loaded.resolve();
});
@ -44,7 +44,7 @@ instance.web_gantt.GanttView = instance.web.View.extend({
n_group_bys = group_bys;
}
// gather the fields to get
var fields = _.compact(_.map(["date_start", "date_delay", "date_stop"], function(key) {
var fields = _.compact(_.map(["date_start", "date_delay", "date_stop", "progress"], function(key) {
return self.fields_view.arch.attrs[key] || '';
}));
fields = _.uniq(fields.concat(n_group_bys));
@ -75,7 +75,7 @@ instance.web_gantt.GanttView = instance.web.View.extend({
on_data_loaded_2: function(tasks, group_bys) {
var self = this;
$(".oe_gantt", this.$el).html("");
//prevent more that 1 group by
if (group_bys.length > 0) {
group_bys = [group_bys[0]];
@ -114,6 +114,11 @@ instance.web_gantt.GanttView = instance.web.View.extend({
var task_ids = {};
// creation of the chart
var generate_task_info = function(task, plevel) {
if (_.isNumber(task[self.fields_view.arch.attrs.progress])) {
var percent = task[self.fields_view.arch.attrs.progress] || 0;
} else {
var percent = 100;
}
var level = plevel || 0;
if (task.__is_group) {
var task_infos = _.compact(_.map(task.tasks, function(sub_task) {
@ -128,7 +133,7 @@ instance.web_gantt.GanttView = instance.web.View.extend({
return memo === undefined || date > memo ? date : memo;
}, undefined);
var duration = (task_stop.getTime() - task_start.getTime()) / (1000 * 60 * 60);
var group_name = instance.web.format_value(task.name, self.fields[group_bys[level]]);
var group_name = task.name ? instance.web.format_value(task.name, self.fields[group_bys[level]]) : "-";
if (level == 0) {
var group = new GanttProjectInfo(_.uniqueId("gantt_project_"), group_name, task_start);
_.each(task_infos, function(el) {
@ -136,7 +141,7 @@ instance.web_gantt.GanttView = instance.web.View.extend({
});
return group;
} else {
var group = new GanttTaskInfo(_.uniqueId("gantt_project_task_"), group_name, task_start, duration || 1, 100);
var group = new GanttTaskInfo(_.uniqueId("gantt_project_task_"), group_name, task_start, duration || 1, percent);
_.each(task_infos, function(el) {
group.addChildTask(el.task_info);
});
@ -151,7 +156,7 @@ instance.web_gantt.GanttView = instance.web.View.extend({
if (self.fields_view.arch.attrs.date_stop) {
task_stop = instance.web.auto_str_to_date(task[self.fields_view.arch.attrs.date_stop]);
if (!task_stop)
return;
task_stop = task_start;
} else { // we assume date_duration is defined
var tmp = instance.web.format_value(task[self.fields_view.arch.attrs.date_delay],
self.fields[self.fields_view.arch.attrs.date_delay]);
@ -161,7 +166,7 @@ instance.web_gantt.GanttView = instance.web.View.extend({
}
var duration = (task_stop.getTime() - task_start.getTime()) / (1000 * 60 * 60);
var id = _.uniqueId("gantt_task_");
var task_info = new GanttTaskInfo(id, task_name, task_start, ((duration / 24) * 8) || 1, 100);
var task_info = new GanttTaskInfo(id, task_name, task_start, ((duration / 24) * 8) || 1, percent);
task_info.internal_task = task;
task_ids[id] = task_info;
return {task_info: task_info, task_start: task_start, task_stop: task_stop};

View File

@ -1,5 +1,5 @@
.openerp a.dropdown-menu-icon {
z-index: 10;
z-index: 1;
position: absolute;
color: #4c4c4c;
right: 8px;
@ -23,7 +23,7 @@
padding: 8px;
border: 1px solid #afafb6;
background: white;
z-index: 900;
z-index: 1;
min-width: 160px;
overflow-x: hidden;
-moz-border-radius: 3px;

View File

@ -246,7 +246,7 @@ instance.web_graph.GraphView = instance.web.View.extend({
var result = [];
var ticks = {};
return obj.call("fields_view_get", [view_id, 'graph']).then(function(tmp) {
return this.alive(obj.call("fields_view_get", [view_id, 'graph', context]).then(function(tmp) {
view_get = tmp;
fields = view_get['fields'];
var toload = _.select(group_by, function(x) { return fields[x] === undefined });
@ -368,7 +368,7 @@ instance.web_graph.GraphView = instance.web.View.extend({
'ticks': _.map(ticks, function(el, key) { return [el, key] })
};
return res;
});
}));
},
// Render the graph and update menu styles

View File

@ -7,6 +7,15 @@
background: url(/web/static/src/img/form_sheetbg.png);
width: 100%;
}
.openerp .oe_kanban_view .oe_kanban_group_length {
text-align: center;
display: none;
}
.openerp .oe_kanban_view .oe_kanban_group_length .oe_tag {
position: relative;
top: 8px;
font-weight: bold;
}
.openerp .oe_kanban_view .ui-sortable-placeholder {
border: 1px solid rgba(0, 0, 0, 0.1);
visibility: visible !important;

View File

@ -146,11 +146,11 @@
text-overflow: ellipsis
.oe_fold_column
.oe_kanban_group_length
position: absolute
top: -1px
right: -14px
float: right
display: block
position: absolute
top: -1px
right: -14px
float: right
display: block
&.oe_kanban_grouped
.oe_kanban_column, .oe_kanban_group_header
width: 185px
@ -217,7 +217,7 @@
z-index: 2
.oe_kanban_header .oe_dropdown_toggle
top: -2px
height: 14px;
height: 14px
.oe_kanban_card, .oe_dropdown_toggle
cursor: pointer
display: inline-block

View File

@ -134,7 +134,9 @@ instance.web_kanban.KanbanView = instance.web.View.extend({
switch (node.tag) {
case 'field':
if (this.fields_view.fields[node.attrs.name].type === 'many2many') {
this.many2manys.push(node.attrs.name);
if (_.indexOf(this.many2manys, node.attrs.name) < 0) {
this.many2manys.push(node.attrs.name);
}
node.tag = 'div';
node.attrs['class'] = (node.attrs['class'] || '') + ' oe_form_field oe_tags';
} else {
@ -226,14 +228,15 @@ instance.web_kanban.KanbanView = instance.web.View.extend({
this.search_domain = domain;
this.search_context = context;
this.search_group_by = group_by;
$.when(this.has_been_loaded).done(function() {
return $.when(this.has_been_loaded).then(function() {
self.group_by = group_by.length ? group_by[0] : self.fields_view.arch.attrs.default_group_by;
self.group_by_field = self.fields_view.fields[self.group_by] || {};
self.grouped_by_m2o = (self.group_by_field.type === 'many2one');
self.$buttons.find('.oe_alternative').toggle(self.grouped_by_m2o);
self.$el.toggleClass('oe_kanban_grouped_by_m2o', self.grouped_by_m2o);
var grouping = new instance.web.Model(self.dataset.model, context, domain).query().group_by(self.group_by);
$.when(grouping).done(function(groups) {
var grouping_fields = self.group_by ? [self.group_by].concat(_.keys(self.aggregates)) : undefined;
var grouping = new instance.web.Model(self.dataset.model, context, domain).query().group_by(grouping_fields);
return self.alive($.when(grouping)).done(function(groups) {
self.remove_no_result();
if (groups) {
self.do_process_groups(groups);
@ -655,6 +658,7 @@ instance.web_kanban.KanbanGroup = instance.web.Widget.extend({
self.view.dataset.ids = ids.concat(self.dataset.ids);
self.do_add_records(records);
self.compute_cards_auto_height();
self.view.postprocess_m2m_tags();
return records;
});
},