diff options
| author | Jiten (OpenERP) <jra@tinyerp.com> | 2012-11-19 11:03:20 +0530 |
|---|---|---|
| committer | Jiten (OpenERP) <jra@tinyerp.com> | 2012-11-19 11:03:20 +0530 |
| commit | df184ad1f99c5fbe4f0ae95c7b87cbb1028635a4 (patch) | |
| tree | 83da437768129e158a3eb54b74687f712613b228 /addons/web/static/src | |
| parent | 4d1787644a2dd5105bd45a35241c97007d97fec5 (diff) | |
| parent | 37209eb5e76a7b373a0f302e1f62be32fd2f37d2 (diff) | |
[MERGE] Merge trunk.
bzr revid: jra@tinyerp.com-20121119053320-to7uhn8mk3xpvq8r
Diffstat (limited to 'addons/web/static/src')
| -rw-r--r-- | addons/web/static/src/css/base.css | 208 | ||||
| -rw-r--r-- | addons/web/static/src/css/base.sass | 180 | ||||
| -rw-r--r-- | addons/web/static/src/js/boot.js | 10 | ||||
| -rw-r--r-- | addons/web/static/src/js/chrome.js | 86 | ||||
| -rw-r--r-- | addons/web/static/src/js/corelib.js | 51 | ||||
| -rw-r--r-- | addons/web/static/src/js/coresetup.js | 31 | ||||
| -rw-r--r-- | addons/web/static/src/js/data.js | 13 | ||||
| -rw-r--r-- | addons/web/static/src/js/formats.js | 36 | ||||
| -rw-r--r-- | addons/web/static/src/js/search.js | 136 | ||||
| -rw-r--r-- | addons/web/static/src/js/testing.js | 393 | ||||
| -rw-r--r-- | addons/web/static/src/js/view_form.js | 230 | ||||
| -rw-r--r-- | addons/web/static/src/js/view_list.js | 33 | ||||
| -rw-r--r-- | addons/web/static/src/js/view_list_editable.js | 100 | ||||
| -rw-r--r-- | addons/web/static/src/js/views.js | 70 | ||||
| -rw-r--r-- | addons/web/static/src/xml/base.xml | 89 |
15 files changed, 1217 insertions, 449 deletions
diff --git a/addons/web/static/src/css/base.css b/addons/web/static/src/css/base.css index 2b9a5bd92f3..d7d8d732762 100644 --- a/addons/web/static/src/css/base.css +++ b/addons/web/static/src/css/base.css @@ -20,20 +20,6 @@ font-style: normal; } -@media print { - .oe_topbar, .oe_leftbar, .oe_loading { - display: none !important; - } -} -.openerp.openerp_webclient_container { - height: 100%; -} - -.text-tag .text-button { - height: auto !important; - min-height: 16px; -} - .openerp { padding: 0; margin: 0; @@ -46,6 +32,9 @@ * http://stackoverflow.com/questions/2855589/replace-input-type-file-by-an-image */ } +.openerp.openerp_webclient_container { + height: 100%; +} .openerp :-moz-placeholder { color: #afafb6 !important; font-style: italic !important; @@ -197,6 +186,10 @@ .openerp .oe_bounce_container { display: inline-block; } +.openerp .text-tag .text-button { + height: auto !important; + min-height: 16px; +} .openerp .ui-tabs { position: static; } @@ -1153,6 +1146,7 @@ height: 40px; width: 157px; margin: 14px 0; + border: 0; } .openerp .oe_footer { position: fixed; @@ -2323,16 +2317,16 @@ text-align: justify; } .openerp .oe_form_editable .oe_form .oe_form_field_integer input { - width: 6em !important; + width: 6em; } .openerp .oe_form_editable .oe_form .oe_form_field_float input { - width: 7em !important; + width: 7em; } .openerp .oe_form_editable .oe_form .oe_form_field_date input { - width: 7.5em !important; + width: 7.5em; } .openerp .oe_form_editable .oe_form .oe_form_field_datetime input { - width: 11.5em !important; + width: 11.5em; } .openerp .oe_hidden_input_file { position: relative; @@ -2378,6 +2372,77 @@ .openerp .oe_form .oe_form_field_image:hover .oe_form_field_image_controls { display: block; } +.openerp .oe_fileupload { + display: inline-block; + clear: both; + width: 100%; +} +.openerp .oe_fileupload .oe_add { + float: left; + position: relative; + width: 100%; + left: 2px; + top: 7px; +} +.openerp .oe_fileupload .oe_add button { + display: inline; + height: 24px; + font-size: 12px; + line-height: 12px; + vertical-align: middle; +} +.openerp .oe_fileupload .oe_add button.oe_attach { + width: 24px; + overflow: hidden; + width: 24px; + overflow: hidden; + background: transparent; + color: #7c7bad; + box-shadow: none; + border: none; + text-shadow: none; +} +.openerp .oe_fileupload .oe_add button.oe_attach .oe_e { + position: relative; + top: -1px; + left: -9px; +} +.openerp .oe_fileupload .oe_add input.oe_form_binary_file { + display: inline-block; + margin-left: -5px; + height: 28px; + width: 52px; + margin-top: -26px; +} +.openerp .oe_fileupload .oe_add .oe_attach_label { + color: #7c7bad; + margin-left: -3px; +} +.openerp .oe_fileupload .oe_attachments { + margin-bottom: 4px; + margin-right: 0px; + font-size: 12px; + border-radius: 2px; + border: solid 1px rgba(124, 123, 173, 0.14); +} +.openerp .oe_fileupload .oe_attachments .oe_attachment { + padding: 2px; + padding-left: 4px; + padding-right: 4px; +} +.openerp .oe_fileupload .oe_attachments .oe_attachment .oe_e { + font-size: 23px; + margin-top: -5px; +} +.openerp .oe_fileupload .oe_attachments .oe_attachment .oe_e:hover { + text-decoration: none; +} +.openerp .oe_fileupload .oe_attachments .oe_attachment:nth-child(odd) { + background: white; +} +.openerp .oe_fileupload .oe_attachments .oe_attachment:nth-child(even) { + background: #f4f5fa; +} .openerp .oe_form_field_many2one td:first-child { position: relative; } @@ -2391,6 +2456,9 @@ float: right; padding-left: 2px; } +.openerp .oe_form_field_many2one input { + padding-right: 13px; +} .openerp.ui-autocomplete li.oe_m2o_dropdown_option a { font-style: italic; padding-left: 2em; @@ -2595,32 +2663,33 @@ top: 5px; } .openerp .oe_list.oe_list_editable.oe_editing .oe_m2o_cm_button { - display: none; + line-height: 19px; } -.openerp .oe_list.oe_list_editable.oe_editing .oe_form_field input, .openerp .oe_list.oe_list_editable.oe_editing .oe_form_field textarea { - height: 27px; +.openerp .oe_list.oe_list_editable.oe_editing .oe_input_icon { + margin-top: 5px; +} +.openerp .oe_list.oe_list_editable.oe_editing .oe_form_field { + min-width: 0; + max-width: none; } .openerp .oe_list.oe_list_editable.oe_editing .oe_form_field input, .openerp .oe_list.oe_list_editable.oe_editing .oe_form_field textarea { + height: 27px; -moz-border-radius: 0; -webkit-border-radius: 0; border-radius: 0; border: 1px solid #aaaaff; margin: 0; } +.openerp .oe_list.oe_list_editable.oe_editing .oe_form_field input, .openerp .oe_list.oe_list_editable.oe_editing .oe_form_field textarea, .openerp .oe_list.oe_list_editable.oe_editing .oe_form_field select { + min-width: 0; +} .openerp .oe_list.oe_list_editable.oe_editing .oe_form_field.oe_form_field_float input, .openerp .oe_list.oe_list_editable.oe_editing .oe_form_field.oe_form_view_integer input { text-align: right; width: 100% !important; } -.openerp .oe_list.oe_list_editable.oe_editing .oe_form_field.oe_form_field_datetime > span, .openerp .oe_list.oe_list_editable.oe_editing .oe_form_field.oe_form_field_date > span { - width: 100% !important; -} .openerp .oe_list.oe_list_editable.oe_editing .oe_form_field.oe_form_field_datetime input.oe_datepicker_master, .openerp .oe_list.oe_list_editable.oe_editing .oe_form_field.oe_form_field_date input.oe_datepicker_master { width: 100% !important; } -.openerp .oe_list.oe_list_editable.oe_editing .oe_form_field .oe_form_field_float, .openerp .oe_list.oe_list_editable.oe_editing .oe_form_field .oe_form_view_integer, .openerp .oe_list.oe_list_editable.oe_editing .oe_form_field.oe_form_field_datetime, .openerp .oe_list.oe_list_editable.oe_editing .oe_form_field.oe_form_field_date { - min-width: 0 !important; - max-width: none !important; -} .openerp .oe_list_group_name { white-space: nowrap; } @@ -2685,6 +2754,9 @@ text-align: right !important; max-width: 100px; } +.openerp .oe_list_content td.oe_list_field_date, .openerp .oe_list_content th.oe_list_header_date { + min-width: 6em; +} .openerp .oe_list_content > thead { border-bottom: 2px solid #cacaca; background: #eeeeee; @@ -2752,6 +2824,10 @@ .openerp .oe_list_content > tbody > tr > td.oe_list_checkbox:first-child:after, .openerp .oe_list_content > tbody > tr th.oe_list_checkbox:first-child:after { border-width: 0; } +.openerp .oe_list_content > tbody > tr > td.oe_list_field_boolean input { + filter: alpha(opacity=50); + opacity: 0.5; +} .openerp .oe_list_content > tbody > tr:nth-child(odd) { background-color: #f0f0fa; background-color: #f0f0fa; @@ -2899,78 +2975,6 @@ color: #333333; } -.openerp .oe_fileupload { - display: inline-block; - clear: both; - width: 100%; -} -.openerp .oe_fileupload .oe_add { - float: left; - position: relative; - width: 100%; - left: 2px; - top: 7px; -} -.openerp .oe_fileupload .oe_add button { - display: inline; - height: 24px; - font-size: 12px; - line-height: 12px; - vertical-align: middle; -} -.openerp .oe_fileupload .oe_add button.oe_attach { - width: 24px; - overflow: hidden; - width: 24px; - overflow: hidden; - background: transparent; - color: #7c7bad; - box-shadow: none; - border: none; - text-shadow: none; -} -.openerp .oe_fileupload .oe_add button.oe_attach .oe_e { - position: relative; - top: -1px; - left: -9px; -} -.openerp .oe_fileupload .oe_add input.oe_form_binary_file { - display: inline-block; - margin-left: -5px; - height: 28px; - width: 52px; - margin-top: -26px; -} -.openerp .oe_fileupload .oe_add .oe_attach_label { - color: #7c7bad; - margin-left: -3px; -} -.openerp .oe_fileupload .oe_attachments { - margin-bottom: 4px; - margin-right: 0px; - font-size: 12px; - border-radius: 2px; - border: solid 1px rgba(124, 123, 173, 0.14); -} -.openerp .oe_fileupload .oe_attachments .oe_attachment { - padding: 2px; - padding-left: 4px; - padding-right: 4px; -} -.openerp .oe_fileupload .oe_attachments .oe_attachment .oe_e { - font-size: 23px; - margin-top: -5px; -} -.openerp .oe_fileupload .oe_attachments .oe_attachment .oe_e:hover { - text-decoration: none; -} -.openerp .oe_fileupload .oe_attachments .oe_attachment:nth-child(odd) { - background: white; -} -.openerp .oe_fileupload .oe_attachments .oe_attachment:nth-child(even) { - background: #f4f5fa; -} - .kitten-mode-activated { background-image: url(http://placekitten.com/g/1365/769); background-size: cover; @@ -3081,8 +3085,8 @@ div.res { .openerp { text-shadow: none; } - .openerp .oe_header_row, .openerp ul.oe_header, .openerp div.oe_mail_thread_action, .openerp .oe_mail_recthread_actions, .openerp .oe_button_box, .openerp .oe_form button, .openerp button.oe_invite, .openerp .oe_form header, .openerp .openerp .oe_notebook > li.ui-state-default { - display: none; + .openerp .oe_header_row, .openerp ul.oe_header, .openerp div.oe_mail_thread_action, .openerp .oe_mail_recthread_actions, .openerp .oe_button_box, .openerp .oe_form button, .openerp button.oe_invite, .openerp .oe_form header, .openerp .openerp .oe_notebook > li.ui-state-default, .openerp .oe_topbar, .openerp .oe_leftbar, .openerp .oe_loading { + display: none !important; } .openerp .oe_list_content button, .openerp .oe_list_content input[type=checkbox] { visibility: hidden; diff --git a/addons/web/static/src/css/base.sass b/addons/web/static/src/css/base.sass index dc550a3ad9f..c349d3c0eff 100644 --- a/addons/web/static/src/css/base.sass +++ b/addons/web/static/src/css/base.sass @@ -140,18 +140,6 @@ $sheet-padding: 16px // }}} -@media print - .oe_topbar, .oe_leftbar, .oe_loading - display: none !important - -.openerp.openerp_webclient_container - height: 100% - -// jQueryUI css bug fixing -.text-tag .text-button - height: auto !important - min-height: 16px - .openerp // Global style {{{ padding: 0 @@ -161,6 +149,8 @@ $sheet-padding: 16px font-size: 13px background: white text-shadow: 0 1px 1px rgba(255, 255, 255, 0.5) + &.openerp_webclient_container + height: 100% // }}} //Placeholder style{{{ \:-moz-placeholder @@ -254,6 +244,11 @@ $sheet-padding: 16px .oe_bounce_container display: inline-block + // Bug lp:1051746 + .text-tag .text-button + height: auto !important + min-height: 16px + // bug noted in jquery ui CSS doesn't seem to occur in IE9, // so remove position:relative .ui-tabs @@ -943,6 +938,7 @@ $sheet-padding: 16px height: 40px width: 157px margin: 14px 0 + border: 0 .oe_footer position: fixed bottom: 0 @@ -1852,13 +1848,13 @@ $sheet-padding: 16px .oe_form_editable .oe_form .oe_form_field_integer input - width: 6em !important + width: 6em .oe_form_field_float input - width: 7em !important + width: 7em .oe_form_field_date input - width: 7.5em !important + width: 7.5em .oe_form_field_datetime input - width: 11.5em !important + width: 11.5em // }}} // FormView.fields_binary {{{ /* http://www.quirksmode.org/dom/inputfile.html @@ -1901,6 +1897,64 @@ $sheet-padding: 16px @include box-sizing(border) &:hover .oe_form_field_image_controls display: block + .oe_fileupload + display: inline-block + clear: both + width: 100% + .oe_add + float: left + position: relative + width: 100% + left: +2px + top: +7px + button + display: inline + height: 24px + font-size: 12px + line-height: 12px + vertical-align: middle + button.oe_attach + width: 24px + overflow: hidden + width: 24px + overflow: hidden + background: transparent + color: #7C7BAD + box-shadow: none + border: none + text-shadow: none + .oe_e + position: relative + top: -1px + left: -9px + input.oe_form_binary_file + display: inline-block + margin-left: -5px + height: 28px + width: 52px + margin-top: -26px + .oe_attach_label + color: #7C7BAD + margin-left: -3px + .oe_attachments + margin-bottom: 4px + margin-right: 0px + font-size: 12px + border-radius: 2px + border: solid 1px rgba(124,123,173,0.14) + .oe_attachment + padding: 2px + padding-left: 4px + padding-right: 4px + .oe_e + font-size: 23px + margin-top: -5px + .oe_e:hover + text-decoration: none + .oe_attachment:nth-child(odd) + background: white + .oe_attachment:nth-child(even) + background: #F4F5FA // }}} // FormView.many2one {{{ .oe_form_field_many2one @@ -1914,6 +1968,8 @@ $sheet-padding: 16px line-height: 14px float: right padding-left: 2px + input + padding-right: 13px &.ui-autocomplete li.oe_m2o_dropdown_option a font-style: italic @@ -2056,26 +2112,26 @@ $sheet-padding: 16px .oe_m2o_drop_down_button top: 5px .oe_m2o_cm_button - display: none + line-height: 19px + .oe_input_icon + margin-top: 5px .oe_form_field + min-width: 0 + max-width: none input, textarea height: $row-height - input, textarea @include radius(0) border: 1px solid #aaf margin: 0 + input, textarea, select + min-width: 0 &.oe_form_field_float,&.oe_form_view_integer input text-align: right width: 100% !important &.oe_form_field_datetime,&.oe_form_field_date - > span - width: 100% !important input.oe_datepicker_master width: 100% !important - .oe_form_field_float,.oe_form_view_integer,&.oe_form_field_datetime,&.oe_form_field_date - min-width: 0 !important - max-width: none !important .oe_list_group_name white-space: nowrap // }}} @@ -2134,6 +2190,8 @@ $sheet-padding: 16px td.oe_number text-align: right !important max-width: 100px + td.oe_list_field_date, th.oe_list_header_date + min-width: 6em > thead border-bottom: 2px solid #cacaca background: #eee @@ -2183,6 +2241,8 @@ $sheet-padding: 16px width: 17px &:after border-width: 0 + > td.oe_list_field_boolean input + @include opacity() > tr:nth-child(odd) background-color: #f0f0fa @include vertical-gradient(#f0f0fa, #eeeef6) @@ -2297,67 +2357,6 @@ $sheet-padding: 16px float: right color: #333 // }}} - -.openerp - .oe_fileupload - display: inline-block - clear: both - width: 100% - .oe_add - float: left - position: relative - width: 100% - left: +2px - top: +7px - button - display: inline - height: 24px - font-size: 12px - line-height: 12px - vertical-align: middle - button.oe_attach - width: 24px - overflow: hidden - width: 24px - overflow: hidden - background: transparent - color: #7C7BAD - box-shadow: none - border: none - text-shadow: none - .oe_e - position: relative - top: -1px - left: -9px - input.oe_form_binary_file - display: inline-block - margin-left: -5px - height: 28px - width: 52px - margin-top: -26px - .oe_attach_label - color: #7C7BAD - margin-left: -3px - .oe_attachments - margin-bottom: 4px - margin-right: 0px - font-size: 12px - border-radius: 2px - border: solid 1px rgba(124,123,173,0.14) - .oe_attachment - padding: 2px - padding-left: 4px - padding-right: 4px - .oe_e - font-size: 23px - margin-top: -5px - .oe_e:hover - text-decoration: none - .oe_attachment:nth-child(odd) - background: white - .oe_attachment:nth-child(even) - background: #F4F5FA - // Kitten Mode {{{ .kitten-mode-activated background-image: url(http://placekitten.com/g/1365/769) @@ -2367,11 +2366,13 @@ $sheet-padding: 16px opacity: 0.70 // }}} +// jQueryUI top level {{{ // The jQuery-ui overlay and Autocomplete are outside the .openerp div, please don't add indentation !!! div.ui-widget-overlay background: black @include opacity(0.3) - +// TODO: I think only the overlay is problematic, the other top level widgets should use $.fn.openerpClass() +// eg: $el.autocomplete().openerpClass(); .ui-widget font-family: "Lucida Grande", Helvetica, Verdana, Arial, sans-serif color: #4c4c4c @@ -2398,6 +2399,7 @@ div.ui-widget-overlay .ui-corner-all @include radius(3px) +// }}} .resolution width: 350px @@ -2443,11 +2445,11 @@ div.res font-size: 10px padding-right: 5px - @media print .openerp - .oe_header_row, ul.oe_header, div.oe_mail_thread_action, .oe_mail_recthread_actions, .oe_button_box, .oe_form button, button.oe_invite, .oe_form header, .openerp .oe_notebook > li.ui-state-default - display: none + .oe_header_row, ul.oe_header, div.oe_mail_thread_action, .oe_mail_recthread_actions, .oe_button_box, .oe_form button, button.oe_invite, .oe_form header, .openerp .oe_notebook > li.ui-state-default, .oe_topbar, .oe_leftbar, .oe_loading + // We use !important here because jQuery adds @style = display: block on elements when using $.fn.show() + display: none !important .oe_list_content button, input[type=checkbox] visibility: hidden @@ -2473,5 +2475,7 @@ div.res background: none .openerp div.oe_mail_wall overflow: hidden !important +// }}} + // au BufWritePost,FileWritePost *.sass :!sass --style expanded --line-numbers <afile> > "%:p:r.css" // vim:tabstop=4:shiftwidth=4:softtabstop=4:fdm=marker: diff --git a/addons/web/static/src/js/boot.js b/addons/web/static/src/js/boot.js index 05ce709e80a..631a7f8c6f5 100644 --- a/addons/web/static/src/js/boot.js +++ b/addons/web/static/src/js/boot.js @@ -19,12 +19,14 @@ /** * OpenERP instance constructor * - * @param {Array} modules list of modules to initialize + * @param {Array|String} modules list of modules to initialize */ init: function(modules) { - // By default only web will be loaded, the rest will be by loaded - // by openerp.web.Session on the first session_authenticate - modules = _.union(['web'], modules || []); + if (modules === "fuck your shit, don't load anything you cunt") { + modules = []; + } else { + modules = _.union(['web'], modules || []); + } var new_instance = { // links to the global openerp _openerp: openerp, diff --git a/addons/web/static/src/js/chrome.js b/addons/web/static/src/js/chrome.js index 7fac7bdad6d..0db21f2397f 100644 --- a/addons/web/static/src/js/chrome.js +++ b/addons/web/static/src/js/chrome.js @@ -48,7 +48,7 @@ instance.web.Notification = instance.web.Widget.extend({ */ instance.web.dialog = function(element) { var result = element.dialog.apply(element, _.rest(_.toArray(arguments))); - result.dialog("widget").addClass("openerp"); + result.dialog("widget").openerpClass(); return result; }; @@ -190,7 +190,14 @@ instance.web.Dialog = instance.web.Widget.extend({ }); instance.web.CrashManager = instance.web.Class.extend({ + init: function() { + this.active = true; + }, + rpc_error: function(error) { + if (!this.active) { + return; + } if (error.data.fault_code) { var split = ("" + error.data.fault_code).split('\n')[0].split(' -- '); if (split.length > 1) { @@ -205,6 +212,9 @@ instance.web.CrashManager = instance.web.Class.extend({ } }, show_warning: function(error) { + if (!this.active) { + return; + } instance.web.dialog($('<div>' + QWeb.render('CrashManager.warning', {error: error}) + '</div>'), { title: "OpenERP " + _.str.capitalize(error.type), buttons: [ @@ -213,7 +223,9 @@ instance.web.CrashManager = instance.web.Class.extend({ }); }, show_error: function(error) { - var self = this; + if (!this.active) { + return; + } var buttons = {}; buttons[_t("Ok")] = function() { $(this).dialog("close"); @@ -410,7 +422,7 @@ instance.web.DatabaseManager = instance.web.Widget.extend({ self.db_list = null; }); var fetch_langs = this.rpc("/web/session/get_lang_list", {}).done(function(result) { - self.lang_list = result.lang_list; + self.lang_list = result; }); return $.when(fetch_db, fetch_langs).done(self.do_render); }, @@ -433,6 +445,7 @@ instance.web.DatabaseManager = instance.web.Widget.extend({ self.$el.find("tr td:first-child").addClass("oe_form_group_cell_label"); self.$el.find("label").addClass("oe_form_label"); self.$el.find("form[name=create_db_form]").validate({ submitHandler: self.do_create }); + self.$el.find("form[name=duplicate_db_form]").validate({ submitHandler: self.do_duplicate }); self.$el.find("form[name=drop_db_form]").validate({ submitHandler: self.do_drop }); self.$el.find("form[name=backup_db_form]").validate({ submitHandler: self.do_backup }); self.$el.find("form[name=restore_db_form]").validate({ submitHandler: self.do_restore }); @@ -518,6 +531,18 @@ instance.web.DatabaseManager = instance.web.Widget.extend({ self.do_action(client_action); }); }, + do_duplicate: function(form) { + var self = this; + var fields = $(form).serializeArray(); + self.rpc("/web/database/duplicate", {'fields': fields}).then(function(result) { + if (result.error) { + self.display_error(result); + return; + } + self.do_notify("Duplicating database", "The database has been duplicated."); + self.start(); + }); + }, do_drop: function(form) { var self = this; var $form = $(form), @@ -618,6 +643,9 @@ instance.web.Login = instance.web.Widget.extend({ this.selected_db = null; this.selected_login = null; this.params = action.params || {}; + if (_.isEmpty(this.params)) { + this.params = $.bbq.getState(true); + } if (this.params.login_successful) { this.on('login_successful', this, this.params.login_successful); @@ -720,8 +748,34 @@ instance.web.Login = instance.web.Widget.extend({ instance.web.client_actions.add("login", "instance.web.Login"); /** + * Redirect to url by replacing window.location + * If wait is true, sleep 1s and wait for the server i.e. after a restart. + */ +instance.web.redirect = function(url, wait) { + // Dont display a dialog if some xmlhttprequest are in progress + if (instance.client && instance.client.crashmanager) { + instance.client.crashmanager.active = false; + } + + var wait_server = function() { + instance.session.rpc("/web/webclient/version_info", {}).done(function() { + window.location = url; + }).fail(function() { + setTimeout(wait_server, 250); + }); + }; + + if (wait) { + setTimeout(wait_server, 1000); + } else { + window.location = url; + } +}; + +/** * Client action to reload the whole interface. - * If params has an entry 'menu_id', it opens the given menu entry. + * If params.menu_id, it opens the given menu entry. + * If params.wait, reload will wait the openerp server to be reachable before reloading */ instance.web.Reload = function(parent, action) { var params = action.params || {}; @@ -737,8 +791,8 @@ instance.web.Reload = function(parent, action) { hash = "#menu_id=" + menu_id; } var url = l.protocol + "//" + l.host + l.pathname + search + hash; - window.onerror = function() {}; - window.location = url; + + instance.web.redirect(url, params.wait); }; instance.web.client_actions.add("reload", "instance.web.Reload"); @@ -748,7 +802,7 @@ instance.web.client_actions.add("reload", "instance.web.Reload"); */ instance.web.HistoryBack = function(parent) { if (!parent.history_back()) { - window.location = '/' + (window.location.search || ''); + instance.web.Home(parent); } }; instance.web.client_actions.add("history_back", "instance.web.HistoryBack"); @@ -756,11 +810,10 @@ instance.web.client_actions.add("history_back", "instance.web.HistoryBack"); /** * Client action to go back home. */ -instance.web.Home = instance.web.Widget.extend({ - init: function(parent) { - window.location = '/' + (window.location.search || ''); - } -}); +instance.web.Home = function(parent, action) { + var url = '/' + (window.location.search || ''); + instance.web.redirect(url, action.params && action.params.wait); +}; instance.web.client_actions.add("home", "instance.web.Home"); instance.web.ChangePassword = instance.web.Widget.extend({ @@ -987,7 +1040,7 @@ instance.web.UserMenu = instance.web.Widget.extend({ if(res.company_id[0] > 1) topbar_name = _.str.sprintf("%s (%s)", topbar_name, res.company_id[1]); self.$el.find('.oe_topbar_name').text(topbar_name); - var avatar_src = _.str.sprintf('%s/web/binary/image?session_id=%s&model=res.users&field=image_small&id=%s', self.session.prefix, self.session.session_id, self.session.uid); + var avatar_src = self.session.url('/web/binary/image', {model:'res.users', field: 'image_small', id: self.session.uid}); $avatar.attr('src', avatar_src); }); }; @@ -999,7 +1052,7 @@ instance.web.UserMenu = instance.web.Widget.extend({ on_menu_settings: function() { var self = this; if (!this.getParent().has_uncommitted_changes()) { - self.rpc("/web/action/load", { action_id: "base.action_res_users_my" }, function(result) { + self.rpc("/web/action/load", { action_id: "base.action_res_users_my" }).done(function(result) { result.res_id = instance.session.uid; self.getParent().action_manager.do_action(result); }); @@ -1030,6 +1083,7 @@ instance.web.Client = instance.web.Widget.extend({ return instance.session.session_bind(this.origin).then(function() { var $e = $(QWeb.render(self._template, {})); self.replaceElement($e); + $e.openerpClass(); self.bind_events(); return self.show_common(); }); @@ -1264,10 +1318,10 @@ instance.web.WebClient = instance.web.Client.extend({ var self = this; var state = event.getState(true); if (!_.isEqual(this._current_state, state)) { - if(state.action_id === undefined && state.menu_id) { + if(!state.action && state.menu_id) { self.menu.has_been_loaded.done(function() { self.menu.do_reload().done(function() { - self.menu.menu_click(state.menu_id) + self.menu.menu_click(state.menu_id); }); }); } else { diff --git a/addons/web/static/src/js/corelib.js b/addons/web/static/src/js/corelib.js index 4012be526e6..06b3940f1e1 100644 --- a/addons/web/static/src/js/corelib.js +++ b/addons/web/static/src/js/corelib.js @@ -774,10 +774,10 @@ instance.web.Widget = instance.web.Class.extend(instance.web.PropertiesMixin, { } return false; }, - rpc: function(url, data, success, error) { - var def = $.Deferred().done(success).fail(error); + rpc: function(url, data, options) { + var def = $.Deferred(); var self = this; - instance.session.rpc(url, data).done(function() { + instance.session.rpc(url, data, options).done(function() { if (!self.isDestroyed()) def.resolve.apply(def, arguments); }).fail(function() { @@ -1231,12 +1231,14 @@ instance.web.JsonRPC = instance.web.Class.extend(instance.web.PropertiesMixin, { * * @param {String} url RPC endpoint * @param {Object} params call parameters + * @param {Object} options additional options for rpc call * @param {Function} success_callback function to execute on RPC call success * @param {Function} error_callback function to execute on RPC call failure * @returns {jQuery.Deferred} jquery-provided ajax deferred */ - rpc: function(url, params) { + rpc: function(url, params, options) { var self = this; + options = options || {}; // url can be an $.ajax option object if (_.isString(url)) { url = { url: url }; @@ -1251,10 +1253,12 @@ instance.web.JsonRPC = instance.web.Class.extend(instance.web.PropertiesMixin, { id: _.uniqueId('r') }; var deferred = $.Deferred(); - this.trigger('request', url, payload); + if (! options.shadow) + this.trigger('request', url, payload); var request = this.rpc_function(url, payload).done( function (response, textStatus, jqXHR) { - self.trigger('response', response); + if (! options.shadow) + self.trigger('response', response); if (!response.error) { if (url.url === '/web/session/eval_domain_and_context') { self.test_eval(params, response.result); @@ -1268,7 +1272,8 @@ instance.web.JsonRPC = instance.web.Class.extend(instance.web.PropertiesMixin, { } ).fail( function(jqXHR, textStatus, errorThrown) { - self.trigger('response_failed', jqXHR); + if (! options.shadow) + self.trigger('response_failed', jqXHR); var error = { code: -32098, message: "XmlHttpRequestError " + errorThrown, @@ -1276,7 +1281,7 @@ instance.web.JsonRPC = instance.web.Class.extend(instance.web.PropertiesMixin, { }; deferred.reject(error, $.Event()); }); - // Allow deferred user to disable on_rpc_error in fail + // Allow deferred user to disable rpc_error call in fail deferred.fail(function() { deferred.fail(function(error, event) { if (!event.isDefaultPrevented()) { @@ -1309,9 +1314,18 @@ instance.web.JsonRPC = instance.web.Class.extend(instance.web.PropertiesMixin, { // extracted from payload to set on the url var data = { session_id: this.session_id, - id: payload.id + id: payload.id, + sid: this.httpsessionid, }; - url.url = this.get_url(url.url); + + var set_sid = function (response, textStatus, jqXHR) { + // If response give us the http session id, we store it for next requests... + if (response.httpsessionid) { + self.httpsessionid = response.httpsessionid; + } + }; + + url.url = this.url(url.url, null); var ajax = _.extend({ type: "GET", dataType: 'jsonp', @@ -1326,7 +1340,7 @@ instance.web.JsonRPC = instance.web.Class.extend(instance.web.PropertiesMixin, { if(payload_url.length < 2000) { // Direct jsonp request ajax.data.r = payload_str; - return $.ajax(ajax); + return $.ajax(ajax).done(set_sid); } else { // Indirect jsonp request var ifid = _.uniqueId('oe_rpc_iframe'); @@ -1364,11 +1378,20 @@ instance.web.JsonRPC = instance.web.Class.extend(instance.web.PropertiesMixin, { }); // append the iframe to the DOM (will trigger the first load) $form.after($iframe); - return deferred; + return deferred.done(set_sid); } }, - get_url: function (file) { - return this.prefix + file; + + url: function(path, params) { + var qs = ''; + if (!_.isNull(params)) { + params = _.extend(params || {}, {session_id: this.session_id}); + if (this.httpsessionid) { + params.sid = this.httpsessionid; + } + qs = '?' + $.param(params); + } + return this.prefix + path + qs; }, }); diff --git a/addons/web/static/src/js/coresetup.js b/addons/web/static/src/js/coresetup.js index 5ff47aa32fa..7ddefa301f2 100644 --- a/addons/web/static/src/js/coresetup.js +++ b/addons/web/static/src/js/coresetup.js @@ -3,10 +3,13 @@ *--------------------------------------------------------*/ var console; if (!console) { - console = {log: function () {}}; -} -if (!console.debug) { - console.debug = console.log; + // Even IE9 only exposes console object if debug window opened + console = {}; + ('log error debug info warn assert clear dir dirxml trace group' + + ' groupCollapsed groupEnd time timeEnd profile profileEnd count' + + ' exception').split(/\s+/).forEach(function(property) { + console[property] = _.identity; + }); } openerp.web.coresetup = function(instance) { @@ -19,9 +22,9 @@ instance.web.Session = instance.web.JsonRPC.extend( /** @lends instance.web.Sess this.name = instance._session_id; this.qweb_mutex = new $.Mutex(); }, - rpc: function(url, params) { + rpc: function(url, params, options) { params.session_id = this.session_id; - return this._super(url, params); + return this._super(url, params, options); }, /** * Setup a sessionm @@ -198,7 +201,7 @@ instance.web.Session = instance.web.JsonRPC.extend( /** @lends instance.web.Sess var self = this; _.each(files, function (file) { $('head').append($('<link>', { - 'href': self.get_url(file), + 'href': self.url(file, null), 'rel': 'stylesheet', 'type': 'text/css' })); @@ -207,11 +210,11 @@ instance.web.Session = instance.web.JsonRPC.extend( /** @lends instance.web.Sess load_js: function(files) { var self = this; var d = $.Deferred(); - if(files.length != 0) { + if(files.length !== 0) { var file = files.shift(); var tag = document.createElement('script'); tag.type = 'text/javascript'; - tag.src = self.get_url(file); + tag.src = self.url(file, null); tag.onload = tag.onreadystatechange = function() { if ( (tag.readyState && tag.readyState != "loaded" && tag.readyState != "complete") || tag.onload_done ) return; @@ -457,6 +460,16 @@ $.fn.getAttributes = function() { } return o; } +$.fn.openerpClass = function(additionalClass) { + // This plugin should be applied on top level elements + additionalClass = additionalClass || ''; + if (!!$.browser.msie) { + additionalClass += ' openerp_ie'; + } + return this.each(function() { + $(this).addClass('openerp ' + additionalClass); + }); +}; /** Jquery extentions */ $.Mutex = (function() { diff --git a/addons/web/static/src/js/data.js b/addons/web/static/src/js/data.js index dcdeb749672..524a88bdc48 100644 --- a/addons/web/static/src/js/data.js +++ b/addons/web/static/src/js/data.js @@ -278,9 +278,10 @@ instance.web.Model = instance.web.Class.extend({ * @param {String} method name of the method to call * @param {Array} [args] positional arguments * @param {Object} [kwargs] keyword arguments + * @param {Object} [options] additional options for the rpc() method * @returns {jQuery.Deferred<>} call result */ - call: function (method, args, kwargs) { + call: function (method, args, kwargs, options) { args = args || []; kwargs = kwargs || {}; if (!_.isArray(args)) { @@ -294,7 +295,7 @@ instance.web.Model = instance.web.Class.extend({ method: method, args: args, kwargs: kwargs - }); + }, options); }, /** * Fetches a Query instance bound to this model, for searching @@ -721,10 +722,10 @@ instance.web.BufferedDataSet = instance.web.DataSetStatic.extend({ this.last_default_get = {}; }, default_get: function(fields, options) { - return this._super(fields, options).done(this.on_default_get); - }, - on_default_get: function(res) { - this.last_default_get = res; + var self = this; + return this._super(fields, options).done(function(res) { + self.last_default_get = res; + }); }, create: function(data) { var cached = {id:_.uniqueId(this.virtual_id_prefix), values: data, diff --git a/addons/web/static/src/js/formats.js b/addons/web/static/src/js/formats.js index 03b2c5b775d..e586b94db6b 100644 --- a/addons/web/static/src/js/formats.js +++ b/addons/web/static/src/js/formats.js @@ -83,6 +83,42 @@ instance.web.strip_raw_chars = function (value) { var normalize_format = function (format) { return Date.normalizeFormat(instance.web.strip_raw_chars(format)); }; + +/** + * Check with a scary heuristic if the value is a bin_size or not. + * If not, compute an approximate size out of the base64 encoded string. + * + * @param {String} value original format + */ +instance.web.binary_to_binsize = function (value) { + if (!value) { + return instance.web.human_size(0); + } + if (value.substr(0, 10).indexOf(' ') == -1) { + // Computing approximate size out of base64 encoded string + // http://en.wikipedia.org/wiki/Base64#MIME + return instance.web.human_size(value.length / 1.37); + } else { + // already bin_size + return value; + } +}; + +/** + * Returns a human readable size + * + * @param {Number} numner of bytes + */ +instance.web.human_size = function(size) { + var units = _t("Bytes,Kb,Mb,Gb,Tb,Pb,Eb,Zb,Yb").split(','); + var i = 0; + while (size >= 1024) { + size /= 1024; + ++i; + } + return size.toFixed(2) + ' ' + units[i]; +}; + /** * Formats a single atomic value based on a field descriptor * diff --git a/addons/web/static/src/js/search.js b/addons/web/static/src/js/search.js index e84ea66ff9a..0f25b2484c3 100644 --- a/addons/web/static/src/js/search.js +++ b/addons/web/static/src/js/search.js @@ -349,7 +349,7 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea $.when(load_view) .then(function(r) { self.search_view_loaded(r) - }).fail(function () { + }, function () { self.ready.reject.apply(null, arguments); }); } @@ -459,7 +459,7 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea complete_global_search: function (req, resp) { $.when.apply(null, _(this.inputs).chain() .invoke('complete', req.term) - .value()).done(function () { + .value()).then(function () { resp(_(_(arguments).compact()).flatten(true)); }); }, @@ -527,7 +527,7 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea childView.on('blurred', self, self.proxy('childBlurred')); }); - $.when.apply(null, started).done(function () { + $.when.apply(null, started).then(function () { var input_to_focus; // options.at: facet inserted at given index, focus next input // otherwise just focus last input @@ -608,7 +608,7 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea // add Filters to this.inputs, need view.controls filled (new instance.web.search.Filters(this)); // add custom filters to this.inputs - (new instance.web.search.CustomFilters(this)); + this.custom_filters = new instance.web.search.CustomFilters(this); // add Advanced to this.inputs (new instance.web.search.Advanced(this)); }, @@ -635,16 +635,41 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea // load defaults var defaults_fetched = $.when.apply(null, _(this.inputs).invoke( - 'facet_for_defaults', this.defaults)).done(function () { - self.query.reset(_(arguments).compact(), {preventSearch: true}); - }); - + 'facet_for_defaults', this.defaults)) + .then(this.proxy('setup_default_query')); + return $.when(drawer_started, defaults_fetched) .done(function () { self.trigger("search_view_loaded", data); self.ready.resolve(); }); }, + setup_default_query: function () { + // Hacky implementation of CustomFilters#facet_for_defaults ensure + // CustomFilters will be ready (and CustomFilters#filters will be + // correctly filled) by the time this method executes. + var custom_filters = this.custom_filters.filters; + if (!_(custom_filters).isEmpty()) { + // Check for any is_default custom filter + var personal_filter = _(custom_filters).find(function (filter) { + return filter.user_id && filter.is_default; + }); + if (personal_filter) { + this.custom_filters.enable_filter(personal_filter, true); + return; + } + + var global_filter = _(custom_filters).find(function (filter) { + return !filter.user_id && filter.is_default; + }); + if (global_filter) { + this.custom_filters.enable_filter(global_filter, true); + return; + } + } + // No custom filter, or no is_default custom filter, apply view defaults + this.query.reset(_(arguments).compact(), {preventSearch: true}); + }, /** * Extract search data from the view's facets. * @@ -1460,10 +1485,15 @@ instance.web.search.ManyToOneField = instance.web.search.CharField.extend({ instance.web.search.CustomFilters = instance.web.search.Input.extend({ template: 'SearchView.CustomFilters', _in_drawer: true, + init: function () { + this.is_ready = $.Deferred(); + this._super.apply(this, arguments); + }, start: function () { var self = this; this.model = new instance.web.Model('ir.filters'); this.filters = {}; + this.$filters = {}; this.view.query .on('remove', function (facet) { if (!facet.get('is_custom_filter')) { @@ -1479,24 +1509,78 @@ instance.web.search.CustomFilters = instance.web.search.Input.extend({ // FIXME: local eval of domain and context to get rid of special endpoint return this.rpc('/web/searchview/get_filters', { model: this.view.model - }).then(this.proxy('set_filters')); + }) + .then(this.proxy('set_filters')) + .then(function () { + self.is_ready.resolve(null); + }, function () { + self.is_ready.reject(); + }); + }, + /** + * Special implementation delaying defaults until CustomFilters is loaded + */ + facet_for_defaults: function () { + return this.is_ready; + }, + /** + * Generates a mapping key (in the filters and $filter mappings) for the + * filter descriptor object provided (as returned by ``get_filters``). + * + * The mapping key is guaranteed to be unique for a given (user_id, name) + * pair. + * + * @param {Object} filter + * @param {String} filter.name + * @param {Number|Pair<Number, String>} [filter.user_id] + * @return {String} mapping key corresponding to the filter + */ + key_for: function (filter) { + var user_id = filter.user_id; + var uid = (user_id instanceof Array) ? user_id[0] : user_id; + return _.str.sprintf('(%s)%s', uid, filter.name); + }, + /** + * Generates a :js:class:`~instance.web.search.Facet` descriptor from a + * filter descriptor + * + * @param {Object} filter + * @param {String} filter.name + * @param {Object} [filter.context] + * @param {Array} [filter.domain] + * @return {Object} + */ + facet_for: function (filter) { + return { + category: _t("Custom Filter"), + icon: 'M', + field: { + get_context: function () { return filter.context; }, + get_groupby: function () { return [filter.context]; }, + get_domain: function () { return filter.domain; } + }, + is_custom_filter: true, + values: [{label: filter.name, value: null}] + }; }, clear_selection: function () { this.$('li.oe_selected').removeClass('oe_selected'); }, append_filter: function (filter) { var self = this; - var key = _.str.sprintf('(%s)%s', filter.user_id, filter.name); + var key = this.key_for(filter); var $filter; - if (key in this.filters) { - $filter = this.filters[key]; + if (key in this.$filters) { + $filter = this.$filters[key]; } else { var id = filter.id; - $filter = this.filters[key] = $('<li></li>') + this.filters[key] = filter; + $filter = this.$filters[key] = $('<li></li>') .appendTo(this.$('.oe_searchview_custom_list')) .addClass(filter.user_id ? 'oe_searchview_custom_private' : 'oe_searchview_custom_public') + .toggleClass('oe_searchview_custom_default', filter.is_default) .text(filter.name); $('<a class="oe_searchview_custom_delete">x</a>') @@ -1504,33 +1588,30 @@ instance.web.search.CustomFilters = instance.web.search.Input.extend({ e.stopPropagation(); self.model.call('unlink', [id]).done(function () { $filter.remove(); + delete self.$filters[key]; + delete self.filters[key]; }); }) .appendTo($filter); } $filter.unbind('click').click(function () { - self.view.query.reset([{ - category: _t("Custom Filter"), - icon: 'M', - field: { - get_context: function () { return filter.context; }, - get_groupby: function () { return [filter.context]; }, - get_domain: function () { return filter.domain; } - }, - is_custom_filter: true, - values: [{label: filter.name, value: null}] - }]); - $filter.addClass('oe_selected'); + self.enable_filter(filter); }); }, + enable_filter: function (filter, preventSearch) { + this.view.query.reset([this.facet_for(filter)], { + preventSearch: preventSearch || false}); + this.$filters[this.key_for(filter)].addClass('oe_selected'); + }, set_filters: function (filters) { _(filters).map(_.bind(this.append_filter, this)); }, save_current: function () { var self = this; var $name = this.$('input:first'); - var private_filter = !this.$('input:last').prop('checked'); + var private_filter = !this.$('#oe_searchview_custom_public').prop('checked'); + var set_as_default = this.$('#oe_searchview_custom_default').prop('checked'); var search = this.view.build_search_data(); this.rpc('/web/session/eval_domain_and_context', { @@ -1546,7 +1627,8 @@ instance.web.search.CustomFilters = instance.web.search.Input.extend({ user_id: private_filter ? instance.session.uid : false, model_id: self.view.model, context: results.context, - domain: results.domain + domain: results.domain, + is_default: set_as_default }; // FIXME: current context? return self.model.call('create_or_replace', [filter]).done(function (id) { diff --git a/addons/web/static/src/js/testing.js b/addons/web/static/src/js/testing.js new file mode 100644 index 00000000000..bf39f543734 --- /dev/null +++ b/addons/web/static/src/js/testing.js @@ -0,0 +1,393 @@ +// Test support structures and methods for OpenERP +openerp.testing = {}; +(function (testing) { + var dependencies = { + corelib: [], + coresetup: ['corelib'], + data: ['corelib', 'coresetup'], + dates: [], + formats: ['coresetup', 'dates'], + chrome: ['corelib', 'coresetup'], + views: ['corelib', 'coresetup', 'data', 'chrome'], + search: ['data', 'coresetup', 'formats'], + list: ['views', 'data'], + form: ['data', 'views', 'list', 'formats'], + list_editable: ['list', 'form', 'data'], + }; + + testing.dependencies = window['oe_all_dependencies'] || []; + testing.current_module = null; + testing.templates = { }; + testing.add_template = function (name) { + var xhr = QWeb2.Engine.prototype.get_xhr(); + xhr.open('GET', name, false); + xhr.send(null); + (testing.templates[testing.current_module] = + testing.templates[testing.current_module] || []) + .push(xhr.responseXML); + }; + /** + * Function which does not do anything + */ + testing.noop = function () { }; + /** + * Alter provided instance's ``session`` attribute to make response + * mockable: + * + * * The ``responses`` parameter can be used to provide a map of (RPC) + * paths (e.g. ``/web/view/load``) to a function returning a response + * to the query. + * * ``instance.session`` grows a ``responses`` attribute which is + * a map of the same (and is in fact initialized to the ``responses`` + * parameter if one is provided) + * + * Note that RPC requests to un-mocked URLs will be rejected with an + * error message: only explicitly specified urls will get a response. + * + * Mocked sessions will *never* perform an actual RPC connection. + * + * @param instance openerp instance being initialized + * @param {Object} [responses] + */ + testing.mockifyRPC = function (instance, responses) { + var session = instance.session; + session.responses = responses || {}; + session.rpc_function = function (url, payload) { + var fn, params; + var needle = payload.params.model + ':' + payload.params.method; + if (url.url === '/web/dataset/call_kw' + && needle in this.responses) { + fn = this.responses[needle]; + params = [ + payload.params.args || [], + payload.params.kwargs || {} + ]; + } else { + fn = this.responses[url.url]; + params = [payload]; + } + + if (!fn) { + return $.Deferred().reject({}, 'failed', + _.str.sprintf("Url %s not found in mock responses, with arguments %s", + url.url, JSON.stringify(payload.params)) + ).promise(); + } + try { + return $.when(fn.apply(null, params)).then(function (result) { + // Wrap for RPC layer unwrapper thingy + return {result: result}; + }); + } catch (e) { + // not sure why this looks like that + return $.Deferred().reject({}, 'failed', String(e)); + } + }; + }; + + var StackProto = { + execute: function (fn) { + var args = [].slice.call(arguments, 1); + // Warning: here be dragons + var i = 0, setups = this.setups, teardowns = this.teardowns; + var d = $.Deferred(); + + var succeeded, failed; + var success = function () { + succeeded = _.toArray(arguments); + return teardown(); + }; + var failure = function () { + // save first failure + if (!failed) { + failed = _.toArray(arguments); + } + // chain onto next teardown + return teardown(); + }; + + var setup = function () { + // if setup to execute + if (i < setups.length) { + var f = setups[i] || testing.noop; + $.when(f.apply(null, args)).then(function () { + ++i; + setup(); + }, failure); + } else { + $.when(fn.apply(null, args)).then(success, failure); + } + }; + var teardown = function () { + // if teardown to execute + if (i > 0) { + var f = teardowns[--i] || testing.noop; + $.when(f.apply(null, args)).then(teardown, failure); + } else { + if (failed) { + d.reject.apply(d, failed); + } else if (succeeded) { + d.resolve.apply(d, succeeded); + } else { + throw new Error("Didn't succeed or fail?"); + } + } + }; + setup(); + + return d; + }, + push: function (setup, teardown) { + return _.extend(Object.create(StackProto), { + setups: this.setups.concat([setup]), + teardowns: this.teardowns.concat([teardown]) + }); + }, + unshift: function (setup, teardown) { + return _.extend(Object.create(StackProto), { + setups: [setup].concat(this.setups), + teardowns: [teardown].concat(this.teardowns) + }); + } + }; + /** + * + * @param {Function} [setup] + * @param {Function} [teardown] + * @return {*} + */ + testing.Stack = function (setup, teardown) { + return _.extend(Object.create(StackProto), { + setups: setup ? [setup] : [], + teardowns: teardown ? [teardown] : [] + }); + }; + + var db = window['oe_db_info']; + testing.section = function (name, options, body) { + if (_.isFunction(options)) { + body = options; + options = {}; + } + _.defaults(options, { + setup: testing.noop, + teardown: testing.noop + }); + + QUnit.module(testing.current_module + '.' + name, {_oe: options}); + body(testing.case); + }; + testing.case = function (name, options, callback) { + if (_.isFunction(options)) { + callback = options; + options = {}; + } + + var module = testing.current_module; + var module_index = _.indexOf(testing.dependencies, module); + var module_deps = testing.dependencies.slice( + // If module not in deps (because only tests, no JS) -> indexOf + // returns -1 -> index becomes 0 -> replace with ``undefined`` so + // Array#slice returns a full copy + 0, module_index + 1 || undefined); + + // Serialize options for this precise test case + // WARNING: typo is from jquery, do not fix! + var env = QUnit.config.currentModuleTestEnviroment; + // section setup + // case setup + // test + // case teardown + // section teardown + var case_stack = testing.Stack() + .push(env._oe.setup, env._oe.teardown) + .push(options.setup, options.teardown); + var opts = _.defaults({}, options, env._oe); + // FIXME: if this test is ignored, will still query + if (opts.rpc === 'rpc' && !db) { + QUnit.config.autostart = false; + db = { + source: null, + supadmin: null, + password: null + }; + var $msg = $('<form style="margin: 0 1em 1em;">') + .append('<h3>A test needs to clone a database</h3>') + .append('<h4>Please provide the source clone information</h4>') + .append(' Source DB: ').append('<input name="source">').append('<br>') + .append(' DB Password: ').append('<input name="supadmin">').append('<br>') + .append('Admin Password: ').append('<input name="password">').append('<br>') + .append('<input type="submit" value="OK"/>') + .submit(function (e) { + e.preventDefault(); + e.stopPropagation(); + db.source = $msg.find('input[name=source]').val(); + db.supadmin = $msg.find('input[name=supadmin]').val(); + db.password = $msg.find('input[name=password]').val(); + QUnit.start(); + $.unblockUI(); + }); + $.blockUI({ + message: $msg, + css: { + fontFamily: 'monospace', + textAlign: 'left', + whiteSpace: 'pre-wrap', + cursor: 'default' + } + }); + } + + QUnit.test(name, function () { + var instance; + if (!opts.dependencies) { + instance = openerp.init(module_deps); + } else { + // empty-but-specified dependencies actually allow running + // without loading any module into the instance + + // TODO: clean up this mess + var d = opts.dependencies.slice(); + // dependencies list should be in deps order, reverse to make + // loading order from last + d.reverse(); + var di = 0; + while (di < d.length) { + var m = /^web\.(\w+)$/.exec(d[di]); + if (m) { + d[di] = m[1]; + } + d.splice.apply(d, [di+1, 0].concat( + _(dependencies[d[di]]).reverse())); + ++di; + } + + instance = openerp.init("fuck your shit, don't load anything you cunt"); + _(d).chain() + .reverse() + .uniq() + .each(function (module) { + openerp.web[module](instance); + }); + } + if (_.isNumber(opts.asserts)) { + expect(opts.asserts); + } + + if (opts.templates) { + for(var i=0; i<module_deps.length; ++i) { + var dep = module_deps[i]; + var templates = testing.templates[dep]; + if (_.isEmpty(templates)) { continue; } + + for (var j=0; j < templates.length; ++j) { + instance.web.qweb.add_template(templates[j]); + } + } + } + + var $fixture = $('#qunit-fixture'); + + var mock, async = false; + switch (opts.rpc) { + case 'mock': + async = true; + testing.mockifyRPC(instance); + mock = function (spec, handler) { + instance.session.responses[spec] = handler; + }; + break; + case 'rpc': + async = true; + (function () { + // Bunch of random base36 characters + var dbname = 'test_' + Math.random().toString(36).slice(2); + // Add db setup/teardown at the start of the stack + case_stack = case_stack.unshift(function (instance) { + // FIXME hack: don't want the session to go through shitty loading process of everything + instance.session.session_init = testing.noop; + instance.session.load_modules = testing.noop; + instance.session.session_bind(); + return instance.session.rpc('/web/database/duplicate', { + fields: [ + {name: 'super_admin_pwd', value: db.supadmin}, + {name: 'db_original_name', value: db.source}, + {name: 'db_name', value: dbname} + ] + }).then(function (result) { + if (result.error) { + return $.Deferred().reject(result.error).promise(); + } + return instance.session.session_authenticate( + dbname, 'admin', db.password, true); + }); + }, function (instance) { + return instance.session.rpc('/web/database/drop', { + fields: [ + {name: 'drop_pwd', value: db.supadmin}, + {name: 'drop_db', value: dbname} + ] + }).then(function (result) { + if (result.error) { + return $.Deferred().reject(result.error).promise(); + } + return result; + }); + }); + })(); + } + + // Always execute tests asynchronously + stop(); + var timeout; + case_stack.execute(function () { + var result = callback.apply(null, arguments); + if (!(result && _.isFunction(result.then))) { + if (async) { + ok(false, "asynchronous test cases must return a promise"); + } + } else { + if (!_.isNumber(opts.asserts)) { + ok(false, "asynchronous test cases must specify the " + + "number of assertions they expect"); + } + } + + return $.Deferred(function (d) { + $.when(result).then(function () { + d.resolve.apply(d, arguments) + }, function () { + d.reject.apply(d, arguments); + }); + if (async || (result && result.then)) { + // async test can be either implicit async (rpc) or + // promise-returning + timeout = setTimeout(function () { + QUnit.config.semaphore = 1; + d.reject({message: "Test timed out"}); + }, 2000); + } + }); + }, instance, $fixture, mock).always(function () { + if (timeout) { clearTimeout(timeout); } + start(); + }).fail(function (error) { + if (options.fail_on_rejection === false) { + return; + } + var message; + if (typeof error !== 'object' + || typeof error.message !== 'string') { + message = JSON.stringify([].slice.apply(arguments)); + } else { + message = error.message; + if (error.data && error.data.debug) { + message += '\n\n' + error.data.debug; + } + } + + ok(false, message); + }); + }); + }; +})(openerp.testing); diff --git a/addons/web/static/src/js/view_form.js b/addons/web/static/src/js/view_form.js index a5077e72260..291fae5e0d8 100644 --- a/addons/web/static/src/js/view_form.js +++ b/addons/web/static/src/js/view_form.js @@ -174,10 +174,14 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM } else { this.$el.find('.oe_form_buttons').replaceWith(this.$buttons); } - this.$buttons.on('click', '.oe_form_button_create', this.on_button_create); - this.$buttons.on('click', '.oe_form_button_edit', this.on_button_edit); - this.$buttons.on('click', '.oe_form_button_save', this.on_button_save); - this.$buttons.on('click', '.oe_form_button_cancel', this.on_button_cancel); + this.$buttons.on('click', '.oe_form_button_create', + this.guard_active(this.on_button_create)); + this.$buttons.on('click', '.oe_form_button_edit', + this.guard_active(this.on_button_edit)); + this.$buttons.on('click', '.oe_form_button_save', + this.guard_active(this.on_button_save)); + this.$buttons.on('click', '.oe_form_button_cancel', + this.guard_active(this.on_button_cancel)); if (this.options.footer_to_buttons) { this.$el.find('footer').appendTo(this.$buttons); } @@ -634,6 +638,8 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM return self._process_save(save_obj).then(function() { save_obj.ret = _.toArray(arguments); return iterate(); + }, function() { + save_obj.error = true; }); } return $.when(); @@ -817,6 +823,8 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM var save_obj = {prepend_on_create: prepend_on_create, ret: null}; this.save_list.push(save_obj); return this._process_operations().then(function() { + if (save_obj.error) + return $.Deferred().reject(); return $.when.apply($, save_obj.ret); }); }, @@ -1203,14 +1211,36 @@ instance.web.form.FormRenderingEngine = instance.web.form.FormRenderingEngineInt }); } }, + view_arch_to_dom_node: function(arch) { + // Historic mess for views arch + // + // server: + // -> got xml as string + // -> parse to xml and manipulate domains and contexts + // -> convert to json + // client: + // -> got view as json + // -> convert back to xml as string + // -> parse it as xml doc (manipulate button@type for IE) + // -> convert back to string + // -> parse it as dom element with jquery + // -> for each widget, convert node to json + // + // Wow !!! + var xml = instance.web.json_node_to_xml(arch); + + var doc = $.parseXML('<div class="oe_form">' + xml + '</div>'); + $('button', doc).each(function() { + $(this).attr('data-button-type', $(this).attr('type')); + }); + xml = instance.web.xml_to_str(doc); + return $(xml); + }, render_to: function($target) { var self = this; this.$target = $target; - // TODO: I know this will save the world and all the kitten for a moment, - // but one day, we will have to get rid of xml2json - var xml = instance.web.json_node_to_xml(this.fvg.arch); - this.$form = $('<div class="oe_form">' + xml + '</div>'); + this.$form = this.view_arch_to_dom_node(this.fvg.arch); this.process_version(); @@ -1879,6 +1909,7 @@ instance.web.form.FormWidget = instance.web.Widget.extend(instance.web.form.Invi instance.web.form.WidgetButton = instance.web.form.FormWidget.extend({ template: 'WidgetButton', init: function(field_manager, node) { + node.attrs.type = node.attrs['data-button-type']; this._super(field_manager, node); this.force_disabled = false; this.string = (this.node.attrs.string || '').replace(/_/g, ''); @@ -2168,17 +2199,10 @@ instance.web.form.AbstractField = instance.web.form.FormWidget.extend(instance.w }, set_dimensions: function (height, width) { - // remove width css property - this.$el.css('width', ''); - // extract style (without width) - var old_style = this.$el.attr('style'); - // jQuery doesn't understand/use !important - var style = 'width:' + width + 'px !important;'; - if (old_style) { - style += old_style - } - this.$el.attr('style', style); - this.$el.css('minHeight', height); + this.$el.css({ + width: width, + minHeight: height + }); }, commit_value: function() { return $.when(); @@ -2402,7 +2426,7 @@ instance.web.DateTimeWidget = instance.web.Widget.extend({ self.$input.focus(); return; } - self.picker('setDate', self.value ? instance.web.auto_str_to_date(self.value) : new Date()); + self.picker('setDate', self.get('value') ? instance.web.auto_str_to_date(self.get('value')) : new Date()); self.$input_picker.show(); self.picker('show'); self.$input_picker.hide(); @@ -3118,7 +3142,7 @@ instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(instanc minLength: 0, delay: 0 }); - this.$input.autocomplete("widget").addClass("openerp"); + this.$input.autocomplete("widget").openerpClass(); // used to correct a bug when selecting an element by pushing 'enter' in an editable list this.$input.keyup(function(e) { if (e.which === 13) { // ENTER @@ -3829,26 +3853,13 @@ instance.web.form.One2ManyListView = instance.web.ListView.extend({ this._super.apply(this, arguments); }, do_delete: function (ids) { - var self = this; - var next = $.when(); - var _super = this._super; - // handle deletion of an item which does not exist - // TODO: better handle that in the editable list? - var false_id_index = _(ids).indexOf(false); - if (false_id_index !== -1) { - ids.splice(false_id_index, 1); - next = this.cancel_edition(true); - } - return next.then(function () { - // wheeee - var confirm = window.confirm; - window.confirm = function () { return true; }; - try { - return _super.call(self, ids); - } finally { - window.confirm = confirm; - } - }); + var confirm = window.confirm; + window.confirm = function () { return true; }; + try { + return this._super(ids); + } finally { + window.confirm = confirm; + } } }); instance.web.form.One2ManyGroups = instance.web.ListView.Groups.extend({ @@ -4011,13 +4022,18 @@ instance.web.form.FieldMany2ManyTags = instance.web.form.AbstractField.extend(in self._drop_shown = true; }); self.tags = self.$text.textext()[0].tags(); - self.$text.focusout(function() { - self.$text.trigger("setInputData", ""); - }).keydown(function(e) { - if (e.which === $.ui.keyCode.TAB && self._drop_shown) { - self.$text.textext()[0].autocomplete().selectFromDropdown(); - } - }); + self.$text + .focusin(function () { + self.trigger('focused'); + }) + .focusout(function() { + self.$text.trigger("setInputData", ""); + self.trigger('blurred'); + }).keydown(function(e) { + if (e.which === $.ui.keyCode.TAB && self._drop_shown) { + self.$text.textext()[0].autocomplete().selectFromDropdown(); + } + }); }, set_value: function(value_) { value_ = value_ || []; @@ -4806,15 +4822,6 @@ instance.web.form.FieldBinary = instance.web.form.AbstractField.extend(instance. this.$el.find('button.oe_form_binary_file_save').click(this.on_save_as); this.$el.find('.oe_form_binary_file_clear').click(this.on_clear); }, - human_filesize : function(size) { - var units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; - var i = 0; - while (size >= 1024) { - size /= 1024; - ++i; - } - return size.toFixed(2) + ' ' + units[i]; - }, on_file_change: function(e) { var self = this; var file_node = e.target; @@ -4862,6 +4869,7 @@ instance.web.form.FieldBinary = instance.web.form.AbstractField.extend(instance. link.href = "data:application/octet-stream;base64," + value; } else { instance.web.blockUI(); + var c = instance.webclient.crashmanager; this.session.get_file({ url: '/web/binary/saveas_ajax', data: {data: JSON.stringify({ @@ -4872,7 +4880,7 @@ instance.web.form.FieldBinary = instance.web.form.AbstractField.extend(instance. context: this.view.dataset.get_context() })}, complete: instance.web.unblockUI, - error: instance.webclient.crashmanager.on_rpc_error + error: c.rpc_error.bind(c) }); ev.stopPropagation(); return false; @@ -4931,7 +4939,7 @@ instance.web.form.FieldBinaryFile = instance.web.form.FieldBinary.extend({ on_file_uploaded_and_valid: function(size, name, content_type, file_base64) { this.binary_value = true; this.internal_set_value(file_base64); - var show_value = name + " (" + this.human_filesize(size) + ")"; + var show_value = name + " (" + instance.web.human_size(size) + ")"; this.$el.find('input').eq(0).val(show_value); this.set_filename(name); }, @@ -4951,12 +4959,16 @@ instance.web.form.FieldBinaryImage = instance.web.form.FieldBinary.extend({ if (this.get('value') && ! /^\d+(\.\d*)? \w+$/.test(this.get('value'))) { url = 'data:image/png;base64,' + this.get('value'); } else if (this.get('value')) { - var id = escape(JSON.stringify(this.view.datarecord.id || null)); + var id = JSON.stringify(this.view.datarecord.id || null); var field = this.name; if (this.options.preview_image) field = this.options.preview_image; - url = '/web/binary/image?session_id=' + this.session.session_id + '&model=' + - this.view.dataset.model +'&id=' + id + '&field=' + field + '&t=' + (new Date().getTime()); + url = this.session.url('/web/binary/image', { + model: this.view.dataset.model, + id: id, + field: field, + t: (new Date().getTime()), + }); } else { url = this.placeholder; } @@ -4995,14 +5007,14 @@ instance.web.form.FieldBinaryImage = instance.web.form.FieldBinary.extend({ * Options on attribute ; "blockui" {Boolean} block the UI or not * during the file is uploading */ -instance.web.form.FieldOne2ManyBinaryMultiFiles = instance.web.form.AbstractField.extend({ +instance.web.form.FieldMany2ManyBinaryMultiFiles = instance.web.form.AbstractField.extend({ template: "FieldBinaryFileUploader", init: function(field_manager, node) { this._super(field_manager, node); this.field_manager = field_manager; this.node = node; - if(this.field.type != "one2many" || this.field.relation != 'ir.attachment') { - throw "The type of the field '"+this.field.string+"' must be a one2many field with a relation to 'ir.attachment' model."; + if(this.field.type != "many2many" || this.field.relation != 'ir.attachment') { + throw "The type of the field '"+this.field.string+"' must be a many2many field with a relation to 'ir.attachment' model."; } this.ds_file = new instance.web.DataSetSearch(this, 'ir.attachment'); this.fileupload_id = _.uniqueId('oe_fileupload_temp'); @@ -5012,21 +5024,85 @@ instance.web.form.FieldOne2ManyBinaryMultiFiles = instance.web.form.AbstractFiel this._super(this); 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 ); + }, get_value: function() { return _.map(this.get('value'), function (value) { return commands.link_to( value.id ); }); }, get_file_url: function (attachment) { - return instance.origin + '/web/binary/saveas?session_id=' + this.session.session_id + '&model=ir.attachment&field=datas&filename_field=datas_fname&id=' + attachment['id']; + 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); + } + }); + // send request for get_name + if (values.length) { + return this.ds_file.call('read', [values, ['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; + } + }); + }); + }); + } else { + return $.when(this.get('value')); + } }, render_value: function () { - var render = $(instance.web.qweb.render('FieldBinaryFileUploader.files', {'widget': this})); - render.on('click', '.oe_delete', _.bind(this.on_file_delete, this)); - this.$('.oe_placeholder_files, .oe_attachments').replaceWith( render ); + var self = this; + this.read_name_values().then(function (datas) { - // reinit input type file - var $input = this.$('input.oe_form_binary_file'); - $input.after($input.clone(true)).remove(); - this.$(".oe_fileupload").show(); + var render = $(instance.web.qweb.render('FieldBinaryFileUploader.files', {'widget': self})); + render.on('click', '.oe_delete', _.bind(self.on_file_delete, self)); + self.$('.oe_placeholder_files, .oe_attachments').replaceWith( render ); + + // reinit input type file + var $input = self.$('input.oe_form_binary_file'); + $input.after($input.clone(true)).remove(); + self.$(".oe_fileupload").show(); + + }); }, on_file_change: function (event) { event.stopPropagation(); @@ -5042,7 +5118,7 @@ instance.web.form.FieldOne2ManyBinaryMultiFiles = instance.web.form.AbstractFiel } // block UI or not - if(this.node.attrs.blockui) { + if(this.node.attrs.blockui>0) { instance.web.blockUI(); } @@ -5076,7 +5152,7 @@ instance.web.form.FieldOne2ManyBinaryMultiFiles = instance.web.form.AbstractFiel }, on_file_loaded: function (event, result) { // unblock UI - if(this.node.attrs.blockui) { + if(this.node.attrs.blockui>0) { instance.web.unblockUI(); } @@ -5106,7 +5182,7 @@ instance.web.form.FieldOne2ManyBinaryMultiFiles = instance.web.form.AbstractFiel if(file_id != this.get('value')[i].id){ files.push(this.get('value')[i]); } - else { + else if(!this.get('value')[i].no_unlink) { this.ds_file.unlink([file_id]); } } @@ -5268,7 +5344,7 @@ instance.web.form.widgets = new instance.web.Registry({ 'progressbar': 'instance.web.form.FieldProgressBar', 'image': 'instance.web.form.FieldBinaryImage', 'binary': 'instance.web.form.FieldBinaryFile', - 'one2many_binary': 'instance.web.form.FieldOne2ManyBinaryMultiFiles', + 'many2many_binary': 'instance.web.form.FieldMany2ManyBinaryMultiFiles', 'statusbar': 'instance.web.form.FieldStatus', 'monetary': 'instance.web.form.FieldMonetary', }); diff --git a/addons/web/static/src/js/view_list.js b/addons/web/static/src/js/view_list.js index 97efb983235..df74674a343 100644 --- a/addons/web/static/src/js/view_list.js +++ b/addons/web/static/src/js/view_list.js @@ -1,6 +1,6 @@ openerp.web.list = function (instance) { var _t = instance.web._t, - _lt = instance.web._lt; + _lt = instance.web._lt; var QWeb = instance.web.qweb; instance.web.views.add('list', 'instance.web.ListView'); instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListView# */ { @@ -503,12 +503,17 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi return this.reload_content(); }, reload_record: function (record) { + var self = this; return this.dataset.read_ids( [record.get('id')], _.pluck(_(this.columns).filter(function (r) { return r.tag === 'field'; }), 'name') ).done(function (records) { + if (!records[0]) { + self.records.remove(record); + return; + } _(records[0]).each(function (value, key) { record.set(key, value, {silent: true}); }); @@ -2151,7 +2156,7 @@ instance.web.list.Boolean = instance.web.list.Column.extend({ * @private */ _format: function (row_data, options) { - return _.str.sprintf('<input type="checkbox" %s disabled="disabled"/>', + return _.str.sprintf('<input type="checkbox" %s readonly="readonly"/>', row_data[this.id].value ? 'checked="checked"' : ''); } }); @@ -2163,20 +2168,24 @@ instance.web.list.Binary = instance.web.list.Column.extend({ */ _format: function (row_data, options) { var text = _t("Download"); - var download_url = _.str.sprintf( - '/web/binary/saveas?session_id=%s&model=%s&field=%s&id=%d', - instance.session.session_id, options.model, this.id, options.id); - if (this.filename) { - download_url += '&filename_field=' + this.filename; - if (row_data[this.filename]) { - text = _.str.sprintf(_t("Download \"%s\""), instance.web.format_value( - row_data[this.filename].value, {type: 'char'})); + var value = row_data[this.id].value; + var download_url; + 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}); + if (this.filename) { + download_url += '&filename_field=' + this.filename; } } - return _.template('<a href="<%-href%>"><%-text%></a> (%<-size%>)', { + if (this.filename && row_data[this.filename]) { + text = _.str.sprintf(_t("Download \"%s\""), instance.web.format_value( + row_data[this.filename].value, {type: 'char'})); + } + return _.template('<a href="<%-href%>"><%-text%></a> (<%-size%>)', { text: text, href: download_url, - size: row_data[this.id].value + size: instance.web.binary_to_binsize(value), }); } }); diff --git a/addons/web/static/src/js/view_list_editable.js b/addons/web/static/src/js/view_list_editable.js index 37ce8c4f8b4..1abe3caab39 100644 --- a/addons/web/static/src/js/view_list_editable.js +++ b/addons/web/static/src/js/view_list_editable.js @@ -77,6 +77,15 @@ openerp.web.list_editable = function (instance) { do_edit: function (index, id, dataset) { _.extend(this.dataset, dataset); }, + do_delete: function (ids) { + var _super = this._super.bind(this); + var next = this.editor.is_editing() + ? this.cancel_edition(true) + : $.when(); + return next.then(function () { + return _super(ids); + }); + }, editable: function () { return this.fields_view.arch.attrs.editable || this._context_editable @@ -117,13 +126,6 @@ openerp.web.list_editable = function (instance) { e.preventDefault(); self.cancel_edition(); }); - this.$el - .off('click', 'tbody td:not(.oe_list_field_cell)') - .on('click', 'tbody td:not(.oe_list_field_cell)', function () { - if (!self.editor.is_editing()) { - self.start_edition(); - } - }); this.editor.destroy(); // Editor is not restartable due to formview not being // restartable @@ -164,13 +166,23 @@ openerp.web.list_editable = function (instance) { * @returns {$.Deferred} */ ensure_saved: function () { - var self = this; - return this.saving_mutex.exec(function() { - if (!self.editor.is_editing()) { - return $.when(); - } - return self.save_edition(); - }); + return this.save_edition(); + }, + /** + * Builds a record with the provided id (``false`` for a creation), + * setting all columns with ``false`` value so code which relies on + * having an actual value behaves correctly + * + * @param {*} id + * @return {instance.web.list.Record} + */ + make_empty_record: function (id) { + var attrs = {id: id}; + _(this.columns).chain() + .filter(function (x) { return x.tag === 'field'}) + .pluck('name') + .each(function (field) { attrs[field] = false; }); + return new instance.web.list.Record(attrs); }, /** * Set up the edition of a record of the list view "inline" @@ -186,12 +198,7 @@ openerp.web.list_editable = function (instance) { if (record) { item = record.attributes; } else { - var attrs = {id: false}; - _(this.columns).chain() - .filter(function (x) { return x.tag === 'field'}) - .pluck('name') - .each(function (field) { attrs[field] = false; }); - record = new instance.web.list.Record(attrs); + record = this.make_empty_record(false); this.records.add(record, { at: this.prepends_on_create() ? 0 : null}); } @@ -269,29 +276,34 @@ openerp.web.list_editable = function (instance) { */ save_edition: function () { var self = this; - return this.with_event('save', { - editor: this.editor, - form: this.editor.form, - cancel: false - }, function () { - return this.editor.save().then(function (attrs) { - var created = false; - var record = self.records.get(attrs.id); - if (!record) { - // new record - created = true; - record = self.records.find(function (r) { - return !r.get('id'); - }).set('id', attrs.id); - } - // onwrite callback could be altering & reloading the - // record which has *just* been saved, so first perform all - // onwrites then do a final reload of the record - return self.handle_onwrite(record) - .then(function () { - return self.reload_record(record); }) - .then(function () { - return { created: created, record: record }; }); + return self.saving_mutex.exec(function() { + if (!self.editor.is_editing()) { + return $.when(); + } + return self.with_event('save', { + editor: self.editor, + form: self.editor.form, + cancel: false + }, function () { + return self.editor.save().then(function (attrs) { + var created = false; + var record = self.records.get(attrs.id); + if (!record) { + // new record + created = true; + record = self.records.find(function (r) { + return !r.get('id'); + }).set('id', attrs.id); + } + // onwrite callback could be altering & reloading the + // record which has *just* been saved, so first perform all + // onwrites then do a final reload of the record + return self.handle_onwrite(record) + .then(function () { + return self.reload_record(record); }) + .then(function () { + return { created: created, record: record }; }); + }); }); }); }, @@ -391,7 +403,7 @@ openerp.web.list_editable = function (instance) { if (!record) { // insert after the source record var index = this.records.indexOf(source_record) + 1; - record = new instance.web.list.Record({id: id}); + record = this.make_empty_record(id); this.records.add(record, {at: index}); this.dataset.ids.splice(index, 0, id); } diff --git a/addons/web/static/src/js/views.js b/addons/web/static/src/js/views.js index 53132e454d1..641821152b2 100644 --- a/addons/web/static/src/js/views.js +++ b/addons/web/static/src/js/views.js @@ -213,8 +213,12 @@ instance.web.ActionManager = instance.web.Widget.extend({ if (run_action) { this.null_action(); action_loaded = this.do_action(state.action); - instance.webclient.menu.has_been_loaded.done(function() { - instance.webclient.menu.open_action(state.action); + $.when(action_loaded || null).done(function() { + instance.webclient.menu.has_been_loaded.done(function() { + if (self.inner_action && self.inner_action.id) { + instance.webclient.menu.open_action(self.inner_action.id); + } + }); }); } } @@ -306,15 +310,15 @@ instance.web.ActionManager = instance.web.Widget.extend({ } var widget = executor.widget(); if (executor.action.target === 'new') { - if (this.dialog_widget && ! this.dialog_widget.isDestroyed()) + if (this.dialog_widget && !this.dialog_widget.isDestroyed()) { this.dialog_widget.destroy(); - if (this.dialog === null || this.dialog.isDestroyed()) { - this.dialog = new instance.web.Dialog(this, { - dialogClass: executor.klass, - }); - this.dialog.on("closing", null, options.on_close); - this.dialog.init_dialog(); } + this.dialog_stop(); + this.dialog = new instance.web.Dialog(this, { + dialogClass: executor.klass, + }); + this.dialog.on("closing", null, options.on_close); + this.dialog.init_dialog(); this.dialog.dialog_title = executor.action.name; if (widget instanceof instance.web.ViewManager) { _.extend(widget.flags, { @@ -397,6 +401,7 @@ instance.web.ActionManager = instance.web.Widget.extend({ }).done(function(res) { action = _.clone(action); action.context = res.context; + var c = instance.webclient.crashmanager; self.session.get_file({ url: '/web/report', data: {action: JSON.stringify(action)}, @@ -407,7 +412,7 @@ instance.web.ActionManager = instance.web.Widget.extend({ } self.dialog_stop(); }, - error: instance.webclient.crashmanager.on_rpc_error + error: c.rpc_error.bind(c) }) }); }, @@ -504,7 +509,7 @@ instance.web.ViewManager = instance.web.Widget.extend({ .find('.oe_view_manager_switch a').filter('[data-view-type="' + view_type + '"]') .parent().addClass('active'); - r = $.when(view_promise).done(function () { + return $.when(view_promise).done(function () { _.each(_.keys(self.views), function(view_name) { var controller = self.views[view_name].controller; if (controller) { @@ -520,7 +525,6 @@ instance.web.ViewManager = instance.web.Widget.extend({ }); self.trigger('switch_mode', view_type, no_store, view_options); }); - return r; }, do_create_view: function(view_type) { // Lazy loading of views @@ -785,8 +789,8 @@ instance.web.ViewManagerAction = instance.web.ViewManager.extend({ name: "JS Tests", target: 'new', type : 'ir.actions.act_url', - url: '/web/static/test/test.html' - }) + url: '/web/tests?mod=*' + }); break; case 'perm_read': var ids = current_view.get_selected_ids(); @@ -1098,11 +1102,11 @@ instance.web.Sidebar = instance.web.Widget.extend({ on_attachments_loaded: function(attachments) { var self = this; var items = []; - var prefix = this.session.origin + '/web/binary/saveas?session_id=' + self.session.session_id + '&model=ir.attachment&field=datas&filename_field=name&id='; + var prefix = this.session.url('/web/binary/saveas', {model: 'ir.attachment', field: 'datas', filename_field: 'name'}); _.each(attachments,function(a) { a.label = a.name; if(a.type === "binary") { - a.url = prefix + a.id + '&t=' + (new Date().getTime()); + a.url = prefix + '&id=' + a.id + '&t=' + (new Date().getTime()); } }); self.items['files'] = attachments; @@ -1235,11 +1239,12 @@ instance.web.View = instance.web.Widget.extend({ }); }, null); } else { + self.do_action({"type":"ir.actions.act_window_close"}); return result_handler(); } }; - if (action_data.special) { + if (action_data.special === 'cancel') { return handler({"type":"ir.actions.act_window_close"}); } else if (action_data.type=="object") { var args = [[record_id]], additional_args = []; @@ -1277,6 +1282,27 @@ instance.web.View = instance.web.Widget.extend({ do_hide: function () { this.$el.hide(); }, + is_active: function () { + var manager = this.getParent(); + return !manager || !manager.active_view + || manager.views[manager.active_view].controller === this; + }, /** + * Wraps fn to only call it if the current view is the active one. If the + * current view is not active, doesn't call fn. + * + * fn can not return anything, as a non-call to fn can't return anything + * either + * + * @param {Function} fn function to wrap in the active guard + */ + guard_active: function (fn) { + var self = this; + return function () { + if (self.is_active()) { + fn.apply(self, arguments); + } + } + }, do_push_state: function(state) { if (this.getParent() && this.getParent().do_push_state) { this.getParent().do_push_state(state); @@ -1384,14 +1410,16 @@ instance.web.json_node_to_xml = function(node, human_readable, indent) { } else { return r + '/>'; } -} +}; instance.web.xml_to_str = function(node) { - if (window.ActiveXObject) { + if (window.XMLSerializer) { + return (new XMLSerializer()).serializeToString(node); + } else if (window.ActiveXObject) { return node.xml; } else { - return (new XMLSerializer()).serializeToString(node); + throw new Error("Could not serialize XML"); } -} +}; instance.web.str_to_xml = function(s) { if (window.DOMParser) { var dp = new DOMParser(); diff --git a/addons/web/static/src/xml/base.xml b/addons/web/static/src/xml/base.xml index 020faf4bd15..8f5ebcab45e 100644 --- a/addons/web/static/src/xml/base.xml +++ b/addons/web/static/src/xml/base.xml @@ -189,6 +189,32 @@ </tr> </table> </form> + <form id="db_duplicate" name="duplicate_db_form" style="display: none;"> + <div class="oe_view_manager oe_view_manager_current"> + <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">Duplicate Database</span> + </h2> + <button type="submit" class="oe_button oe_highlight db_duplicate">Duplicate</button> + </div> + </div> + </div> + <table align="center" class="db_option_table"> + <tr> + <td><label for="super_admin_pwd">Master password:</label></td> + <td><input type="password" name="super_admin_pwd" class="required" value="admin" /></td> + </tr> + <tr> + <td><label for="db_original_name">Original database name:</label></td> + <td><input type="text" name="db_original_name" class="required" matches="^[a-zA-Z][a-zA-Z0-9_-]+$" autofocus="true"/></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_-]+$" /></td> + </tr> + </table> + </form> <form id="db_drop" name="drop_db_form" style="display: none; "> <div class="oe_view_manager oe_view_manager_current"> <div class="oe_view_manager_header" style="padding: 8px;"> @@ -316,6 +342,7 @@ <div class="oe_secondary_menu_section">Database Management</div> <ul class="oe_secondary_submenu"> <li><a href="#db_create">Create</a></li> + <li><a href="#db_duplicate">Duplicate</a></li> <li><a href="#db_drop">Drop</a></li> <li><a href="#db_backup">Backup</a></li> <li><a href="#db_restore">Restore</a></li> @@ -1219,26 +1246,28 @@ </t> <t t-name="FieldBinaryFileUploader.files"> <div class="oe_attachments"> - <t t-if="!widget.get('effective_readonly')" t-foreach="widget.get('value')" t-as="file"> - <div class="oe_attachment"> - <span t-if="(file.upload or file.percent_loaded<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> - <t t-raw="file.name || file.filename"/> - </span> - <a t-if="(!file.upload or file.percent_loaded>=100)" t-att-href="file.url" t-attf-title="{(file.name || file.filename) + (file.date?' \n('+file.date+')':'' )}"> - <t t-raw="file.name || file.filename"/> - </a> - <t t-if="(!file.upload or file.percent_loaded>=100)"> - <a class="oe_right oe_delete oe_e" title="Delete this file" t-attf-data-id="{file.id}">[</a> - </t> - </div> - </t> - <t t-if="widget.get('effective_readonly')" t-foreach="widget.get('value')" t-as="file"> - <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"/> - </a> - </div> + <t t-if="widget.get('value')"> + <t t-if="!widget.get('effective_readonly')" t-foreach="widget.get('value')" t-as="file"> + <div class="oe_attachment"> + <span t-if="(file.upload or file.percent_loaded<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> + <t t-raw="file.name || file.filename"/> + </span> + <a t-if="(!file.upload or file.percent_loaded>=100)" t-att-href="file.url" t-attf-title="{(file.name || file.filename) + (file.date?' \n('+file.date+')':'' )}"> + <t t-raw="file.name || file.filename"/> + </a> + <t t-if="(!file.upload or file.percent_loaded>=100)"> + <a class="oe_right oe_delete oe_e" title="Delete this file" t-attf-data-id="{file.id}">[</a> + </t> + </div> + </t> + <t t-if="widget.get('effective_readonly')" t-foreach="widget.get('value')" t-as="file"> + <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"/> + </a> + </div> + </t> </t> </div> </t> @@ -1549,17 +1578,19 @@ <h3><span class="oe_i">M</span> Custom Filters</h3> <ul class="oe_searchview_custom_list"/> <div class="oe_searchview_custom"> - <h4>Save current filter</h4> - <form> - <p><input id="oe_searchview_custom_input" placeholder="Filter name"/></p> - <p><input id="oe_searchview_custom_public" type="checkbox"/> - <label for="oe_searchview_custom_public">Share with all users</label></p> - <button>Save</button> - </form> + <h4>Save current filter</h4> + <form> + <p><input id="oe_searchview_custom_input" placeholder="Filter name"/></p> + <p> + <input id="oe_searchview_custom_public" type="checkbox"/> + <label for="oe_searchview_custom_public">Share with all users</label> + <input id="oe_searchview_custom_default" type="checkbox"/> + <label for="oe_searchview_custom_default">Use by default</label> + </p> + <button>Save</button> + </form> </div> </div> - <div> - </div> </div> <div t-name="SearchView.advanced" class="oe_searchview_advanced"> |
